[gnome-builder/wip/chergert/refactor] land the refactor
- From: Christian Hergert <chergert src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gnome-builder/wip/chergert/refactor] land the refactor
- Date: Wed, 9 Jan 2019 07:06:24 +0000 (UTC)
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 (¬e_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", ¬e_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 (¬e_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", ¬e_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 (©) && (gtk_text_iter_compare (©, &end_bounds) < 0))
+ {
+ gunichar ch;
+
+ ch = gtk_text_iter_get_char (©);
+
+ 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 (©) && (gtk_text_iter_compare (©, &end_bounds) < 0))
+ {
+ gunichar ch;
+ gboolean invalid = FALSE;
+
+ ch = gtk_text_iter_get_char (©);
+
+ 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 (©)))
+ if (!gtk_text_iter_forward_char (©))
+ break;
+ if (gtk_text_iter_compare (©, 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 (©);
+ }
+
+ 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 (¬ify));
+}
+
+/**
+ * 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 (¬ify));
+}
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 (¬if);
+}
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,
+ ¬if,
+ 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,
+ ¬if,
+ 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,
+ ¬if,
+ 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:
+ *
+ * "(?<filename>[a-zA-Z0-9\\-\\.\\/_]+):"
+ * "(?<line>\\d+):"
+ * "(?<column>\\d+): "
+ * "(?<level>[\\w\\s]+): "
+ * "(?<message>.*)"
+ *
+ * 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 > 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 (¬if), 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 = ¶m[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 (©);
+ }
+
+ 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,
+ ¬if,
+ 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"><primary>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"><primary>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"><primary>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"><Control>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 (©_begin, ©_end);
+
+ begin.line = gtk_text_iter_get_line (©_begin);
+ begin.column = gtk_text_iter_get_line_offset (©_begin);
+
+ end.line = gtk_text_iter_get_line (©_end);
+ end.column = gtk_text_iter_get_line_offset (©_end);
+
+ length = gtk_text_iter_get_offset (©_end) - gtk_text_iter_get_offset (©_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"><control>plus</attribute>
+ </item>
+ <item>
+ <attribute name="label" translatable="yes">Zoom _Out</attribute>
+ <attribute name="action">sourceview.decrease-font-size</attribute>
+ <attribute name="accel"><control>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"><control>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 <b> 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, "<", 4);
+ break;
+
+ case '>':
+ g_string_append_len (str, ">", 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 (©, &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 (©, &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 (©, &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 (©, &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 (©);
+}
+
+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"><alt>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 (¬if);
+}
+
+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"><primary>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"><primary>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"><primary>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"><alt>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"><alt>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"><primary>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"><primary>s</attribute>
+ </item>
+ <item>
+ <attribute name="action">editor-page.save-as</attribute>
+ <attribute name="label" translatable="yes">Save _As</attribute>
+ <attribute name="accel"><primary><shift>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"><primary>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"><primary>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 (©);
+
+ 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 <a href="action://run-manager.run-with-handler::profiler">Run with profiler</a>
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"><alt>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]