diff --git a/data/gsettings/org.gnome.builder.gschema.xml b/data/gsettings/org.gnome.builder.gschema.xml index e687d3baf204fff6aa1f4711a103803e56c89dae..24fa95836efc3b994f34957d6cdf238ca7d928fe 100644 --- a/data/gsettings/org.gnome.builder.gschema.xml +++ b/data/gsettings/org.gnome.builder.gschema.xml @@ -48,5 +48,8 @@ Clear build caches at startup If enabled, Builder will clear build caches upon startup. + + false + diff --git a/meson_options.txt b/meson_options.txt index 657e9beaa4876a2ce9917e1029d9e6aa560ccd77..8599060a0d12a995aa47b674db9bc1e89a39c9f7 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -24,6 +24,7 @@ option('plugin_beautifier', type: 'boolean') option('plugin_c_pack', type: 'boolean') option('plugin_cargo', type: 'boolean') option('plugin_clang', type: 'boolean') +option('plugin_clang_format', type: 'boolean') option('plugin_cmake', type: 'boolean') option('plugin_codespell', type: 'boolean') option('plugin_code_index', type: 'boolean') diff --git a/src/libide/code/ide-file-settings.defs b/src/libide/code/ide-file-settings.defs index 4682acc0bf5c3ac00defa93ba7b9eb0cfbe594f1..ab9886c7d82893386dba63d21282549efab9592e 100644 --- a/src/libide/code/ide-file-settings.defs +++ b/src/libide/code/ide-file-settings.defs @@ -149,4 +149,3 @@ IDE_FILE_SETTINGS_PROPERTY (TRIM_TRAILING_WHITESPACE, trim_trailing_whitespace, (G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)), priv->trim_trailing_whitespace = !!trim_trailing_whitespace;, boolean) - diff --git a/src/libide/gui/ide-preferences-builtin.c b/src/libide/gui/ide-preferences-builtin.c index bdd471a90addea063eaba279d2b84b67e5349bdc..de0e2f00f8e54f91cdd86f3a4005c24a63840532 100644 --- a/src/libide/gui/ide-preferences-builtin.c +++ b/src/libide/gui/ide-preferences-builtin.c @@ -151,6 +151,7 @@ ide_preferences_builtin_register_editor (DzlPreferences *preferences) 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_switch (preferences, "editor", "general", "org.gnome.builder", "format-on-save", NULL, NULL, _("Reformat code on save"), _("Reformat current file on save"), NULL, 5); 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); diff --git a/src/plugins/clang-format/clang-format-plugin.c b/src/plugins/clang-format/clang-format-plugin.c new file mode 100644 index 0000000000000000000000000000000000000000..c11df12a93b2c43cb60451bf63d7fe29024797cc --- /dev/null +++ b/src/plugins/clang-format/clang-format-plugin.c @@ -0,0 +1,34 @@ +/* clang-format-plugin.c + * + * Copyright 2021 Tomi Lähteenmäki + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "config.h" + +#include +#include + +#include "gb-clang-format-buffer-addin.h" + +_IDE_EXTERN void +_gb_clang_format_register_types (PeasObjectModule *module) +{ + peas_object_module_register_extension_type (module, + IDE_TYPE_BUFFER_ADDIN, + GB_TYPE_CLANG_FORMAT_BUFFER_ADDIN); +} diff --git a/src/plugins/clang-format/clang-format.gresource.xml b/src/plugins/clang-format/clang-format.gresource.xml new file mode 100644 index 0000000000000000000000000000000000000000..37c2320eda33263192d2691cabc99c87f4536d9e --- /dev/null +++ b/src/plugins/clang-format/clang-format.gresource.xml @@ -0,0 +1,6 @@ + + + + clang-format.plugin + + diff --git a/src/plugins/clang-format/clang-format.plugin b/src/plugins/clang-format/clang-format.plugin new file mode 100644 index 0000000000000000000000000000000000000000..308b009f5d79af9fbccd1c77892b59894e00e9a9 --- /dev/null +++ b/src/plugins/clang-format/clang-format.plugin @@ -0,0 +1,10 @@ +[Plugin] +Authors=Tomi Lähteenmäki +Builtin=true +Copyright=Copyright © 2021 Tomi Lähteenmäki +Depends=editor; +Description=Format code based on project .clang-format config +Hidden=false +Embedded=_gb_clang_format_register_types +Module=clang-format +Name=ClangFormat diff --git a/src/plugins/clang-format/gb-clang-format-buffer-addin.c b/src/plugins/clang-format/gb-clang-format-buffer-addin.c new file mode 100644 index 0000000000000000000000000000000000000000..b49f4b0e3d90b3e6ec2a4ea877962cb682a7084c --- /dev/null +++ b/src/plugins/clang-format/gb-clang-format-buffer-addin.c @@ -0,0 +1,355 @@ +/* gb-clang-format-buffer-addin.c + * + * Copyright 2021 Tomi Lähteenmäki + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "clang-format-buffer" + +#include "config.h" + +#include +#include +#include +#include + +#include "gb-clang-format-buffer-addin.h" + +struct _GbClangFormatBufferAddin +{ + GObject parent_instance; + gchar *working_directory; + gssize cursor_position; + IdePage *page; + gssize header_len; +}; + +typedef struct { + IdeBuffer *buffer; + IdePage *page; +} Lookup; + +static void +foreach_page_cb (GtkWidget *page, + gpointer user_data) +{ + Lookup *l = user_data; + + if (l->page == NULL && + IDE_IS_EDITOR_PAGE (page) && + ide_editor_page_get_buffer (IDE_EDITOR_PAGE (page)) == l->buffer) + l->page = IDE_PAGE (page); +} + +static IdePage * +get_page (IdeBuffer *buffer) +{ + Lookup lookup = {buffer, NULL}; + IdeContext *context = ide_buffer_ref_context (buffer); + IdeWorkbench *workbench = ide_workbench_from_context (context); + ide_workbench_foreach_page (workbench, foreach_page_cb, &lookup); + + return lookup.page; +} + +gboolean +format_on_save_enabled (IdeBuffer *buffer) +{ + g_autoptr (GSettings) settings = g_settings_new ("org.gnome.builder"); + + return g_settings_get_boolean (settings, "format-on-save"); +} + +gboolean +is_formattable_language (IdeBuffer *buffer) +{ + const gchar *lang_id; + + lang_id = ide_buffer_get_language_id (buffer); + if (lang_id == NULL) + { + g_debug ("Language ID was NULL"); + return FALSE; + } + + if (strcmp (lang_id, "c") == 0 || strcmp (lang_id, "chdr") == 0 || + strcmp (lang_id, "cpp") == 0 || strcmp (lang_id, "cpphdr") == 0 || + strcmp (lang_id, "objc") == 0) + return TRUE; + + return FALSE; +} + +char * +project_root_directory (IdeBuffer *buffer) +{ + IdeContext *context; + GFile *workdir; + + context = ide_buffer_ref_context (buffer); + if (context == NULL) + { + g_warning ("Failed to get IdeContext"); + return NULL; + } + + workdir = ide_context_ref_workdir (context); + if (workdir == NULL) + { + g_warning ("Failed to get working directory"); + return NULL; + } + + return g_file_get_path (workdir); +} + +gboolean +clang_format_config_exists (GbClangFormatBufferAddin *self, + IdeBuffer *buffer) +{ + GFile *config; + + config = g_file_new_build_filename (self->working_directory, ".clang-format", NULL); + + return g_file_query_exists (config, NULL); +} + +gint +get_cursor_position (IdeBuffer *buffer) +{ + GtkTextBuffer *textbuffer; + gint cursor_position; + + textbuffer = GTK_TEXT_BUFFER (buffer); + g_object_get (G_OBJECT (textbuffer), "cursor-position", &cursor_position, NULL); + + return cursor_position; +} + +gssize +get_header_length (gchar *data) +{ + gssize len = g_utf8_strlen (data, G_MAXSSIZE); + + for (gssize i = 0; i < len; ++i) + if (data[i] == '\n') + return (i + 1 <= len ? (i + 1) : i); + + return 0; +} + +gboolean +parse_header (GbClangFormatBufferAddin *self, + gchar *data) +{ + JsonParser *parser; + gboolean ret; + GError *error; + JsonNode *root; + JsonObject *cursor; + + self->header_len = get_header_length (data); + if (self->header_len <= 0) + { + g_warning ("Empty header"); + return FALSE; + } + + error = NULL; + parser = json_parser_new (); + ret = json_parser_load_from_data (parser, data, self->header_len, &error); + if (ret == FALSE) + { + g_warning ("Unable to parse JSON: %s", error->message); + g_error_free (error); + g_object_unref (parser); + return FALSE; + } + + root = json_parser_get_root (parser); + cursor = json_node_get_object (root); + if (cursor == NULL) + { + g_warning ("clang-format didn't return cursor position"); + g_object_unref (parser); + return FALSE; + } + self->cursor_position = json_object_get_int_member (cursor, "Cursor"); + + g_object_unref (parser); + return TRUE; +} + +gchar * +format_cursor_arg (gssize position) +{ + return g_strdup_printf ("--cursor=%zu", position); +} + +IdeSubprocess * +create_process (GbClangFormatBufferAddin *self) +{ + g_autoptr (IdeSubprocessLauncher) launcher = NULL; + IdeSubprocess *subprocess = NULL; + GPtrArray *args; + GError *error = NULL; + gchar *cursor_arg; + + cursor_arg = format_cursor_arg (self->cursor_position); + + args = g_ptr_array_new (); + g_ptr_array_add (args, (gchar *)"clang-format"); + g_ptr_array_add (args, cursor_arg); + g_ptr_array_add (args, NULL); + + launcher = ide_subprocess_launcher_new (G_SUBPROCESS_FLAGS_STDIN_PIPE + | G_SUBPROCESS_FLAGS_STDOUT_PIPE + | G_SUBPROCESS_FLAGS_STDERR_PIPE); + ide_subprocess_launcher_set_cwd (launcher, self->working_directory); + ide_subprocess_launcher_set_argv (launcher, + (const gchar *const *)args->pdata); + subprocess = ide_subprocess_launcher_spawn (launcher, NULL, &error); + + g_ptr_array_free (args, TRUE); + g_free (cursor_arg); + return subprocess; +} + +gboolean +process_communicate (IdeSubprocess *process, + IdeBuffer *buffer, + gchar **stdout_buf) +{ + const gchar *stdin_buf; + gchar *stderr_buf = NULL; + GError *error = NULL; + gboolean ret; + + stdin_buf = g_bytes_get_data (ide_buffer_dup_content (buffer), NULL); + + ret = ide_subprocess_communicate_utf8 (process, stdin_buf, NULL, stdout_buf, + &stderr_buf, &error); + if (ret == FALSE) + { + g_warning ("clang-format failed: %s", error->message); + g_free (error); + return FALSE; + } + + return TRUE; +} + +void +run_clang_format (GbClangFormatBufferAddin *self, + IdeBuffer *buffer) +{ + IdeSubprocess *process; + gchar *stdout_buf = NULL; + gchar *data_start; + gsize data_len; + GtkTextIter cursor; + + process = create_process (self); + if (process_communicate (process, buffer, &stdout_buf) == FALSE) + return; + + if (parse_header (self, stdout_buf) == FALSE) + return; + + data_start = stdout_buf + self->header_len; + data_len = strlen (data_start); + if (data_len <= 0) + { + g_warning ("No output"); + return; + } + + if (data_start[data_len - 1] == '\n') + data_len--; + + gtk_text_buffer_begin_user_action (GTK_TEXT_BUFFER (buffer)); + + gtk_text_buffer_set_text (GTK_TEXT_BUFFER (buffer), data_start, data_len); + + gtk_text_buffer_get_start_iter (GTK_TEXT_BUFFER (buffer), &cursor); + gtk_text_iter_set_offset (&cursor, self->cursor_position); + gtk_text_buffer_place_cursor (GTK_TEXT_BUFFER (buffer), &cursor); + + gtk_text_buffer_end_user_action (GTK_TEXT_BUFFER (buffer)); + + if (self->page != NULL) + { + IdeSourceView *source_view = ide_editor_page_get_view (IDE_EDITOR_PAGE (self->page)); + gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (source_view), &cursor, 0.25, FALSE, 0.5, 0.5); + } + else + g_warning ("Failed to get page"); +} + +static void +gb_clang_format_buffer_addin_save_file (IdeBufferAddin *addin, + IdeBuffer *buffer, + GFile *file) +{ + GbClangFormatBufferAddin *self; + + if (format_on_save_enabled (buffer) == FALSE) + return; + + if (is_formattable_language (buffer) == FALSE) + return; + + self = (GbClangFormatBufferAddin *)addin; + self->working_directory = project_root_directory (buffer); + if (self->working_directory == NULL) + { + g_warning ("Failed to get working directory"); + return; + } + + if (clang_format_config_exists (self, buffer) == FALSE) + { + g_debug ("No .clang-format"); + return; + } + self->cursor_position = get_cursor_position (buffer); + self->page = get_page (buffer); + + run_clang_format (self, buffer); +} + +static void +buffer_addin_iface_init (IdeBufferAddinInterface *iface) +{ + iface->load = NULL; + iface->unload = NULL; + iface->save_file = gb_clang_format_buffer_addin_save_file; + iface->file_loaded = NULL; +} + +G_DEFINE_FINAL_TYPE_WITH_CODE (GbClangFormatBufferAddin, gb_clang_format_buffer_addin, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (IDE_TYPE_BUFFER_ADDIN, buffer_addin_iface_init)) + +static void +gb_clang_format_buffer_addin_class_init (GbClangFormatBufferAddinClass *klass) +{ +} + +static void +gb_clang_format_buffer_addin_init (GbClangFormatBufferAddin *self) +{ +} diff --git a/src/plugins/clang-format/gb-clang-format-buffer-addin.h b/src/plugins/clang-format/gb-clang-format-buffer-addin.h new file mode 100644 index 0000000000000000000000000000000000000000..eb866373cd05a7716057d6b0462431fda55494a2 --- /dev/null +++ b/src/plugins/clang-format/gb-clang-format-buffer-addin.h @@ -0,0 +1,31 @@ +/* gb-clang-format-buffer-addin.h + * + * Copyright 2021 Tomi Lähteenmäki + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define GB_TYPE_CLANG_FORMAT_BUFFER_ADDIN (gb_clang_format_buffer_addin_get_type()) + +G_DECLARE_FINAL_TYPE (GbClangFormatBufferAddin, gb_clang_format_buffer_addin, GB, CLANG_FORMAT_BUFFER_ADDIN, GObject) + +G_END_DECLS diff --git a/src/plugins/clang-format/meson.build b/src/plugins/clang-format/meson.build new file mode 100644 index 0000000000000000000000000000000000000000..d084a203287685d89eae93284f925c907a9389ff --- /dev/null +++ b/src/plugins/clang-format/meson.build @@ -0,0 +1,16 @@ +plugins_sources += files([ + 'clang-format-plugin.c', + 'gb-clang-format-buffer-addin.c', +]) + +plugin_clang_format_resources = gnome.compile_resources( + 'gb-clang-format-resources', + 'clang-format.gresource.xml', + c_name: 'gb_clang_format', +) + +plugins_sources += plugin_clang_format_resources + +if not find_program('clang-format', required: false).found() + message('Please install clang-format as runtime dependency') +endif diff --git a/src/plugins/meson.build b/src/plugins/meson.build index fe7ee0ae93b015459d6cc14b2acaac0223b37591..582cd1727ab092bd7f1c1dd064939575dd406d1a 100644 --- a/src/plugins/meson.build +++ b/src/plugins/meson.build @@ -45,6 +45,7 @@ subdir('buildui') subdir('buffer-monitor') subdir('cargo') subdir('clang') +subdir('clang-format') subdir('cmake') subdir('codespell') subdir('code-index') @@ -153,6 +154,7 @@ status += [ 'C Pack ................ : @0@'.format(get_option('plugin_c_pack')), 'Cargo ................. : @0@'.format(get_option('plugin_cargo')), 'Clang ................. : @0@'.format(get_option('plugin_clang')), + 'ClangFormat ........... : @0@'.format(get_option('plugin_clang_format')), 'CMake ................. : @0@'.format(get_option('plugin_cmake')), 'Codespell ............. : @0@'.format(get_option('plugin_codespell')), 'Code Index ............ : @0@'.format(get_option('plugin_code_index')),