/* * templates.c - this file is part of Geany, a fast and lightweight IDE * * Copyright 2005-2011 Enrico Tröger * Copyright 2006-2011 Nick Treleaven * * This program is free software; you can 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. * * $Id$ */ /* * Templates to insert into the current document, or file templates to create a new * document from. */ #include #include #include "geany.h" #include "templates.h" #include "support.h" #include "utils.h" #include "document.h" #include "encodings.h" #include "editor.h" #include "filetypes.h" #include "ui_utils.h" #include "toolbar.h" #include "geanymenubuttonaction.h" #include "project.h" GeanyTemplatePrefs template_prefs; static GtkWidget *new_with_template_menu = NULL; /* submenu used for both file menu and toolbar */ /* TODO: implement custom insertion templates instead? */ static gchar *templates[GEANY_MAX_TEMPLATES]; static void replace_static_values(GString *text); static gchar *get_template_fileheader(GeanyFiletype *ft); /* called by templates_replace_common */ static void templates_replace_default_dates(GString *text); static void templates_replace_command(GString *text, const gchar *file_name, const gchar *file_type, const gchar *func_name); static gchar *read_file(const gchar *locale_fname) { gchar *contents; gsize length; GString *str; if (! g_file_get_contents(locale_fname, &contents, &length, NULL)) return NULL; if (! encodings_convert_to_utf8_auto(&contents, &length, NULL, NULL, NULL, NULL)) { gchar *utf8_fname = utils_get_utf8_from_locale(locale_fname); ui_set_statusbar(TRUE, _("Failed to convert template file \"%s\" to UTF-8"), utf8_fname); g_free(utf8_fname); g_free(contents); return NULL; } str = g_string_new(contents); g_free(contents); /* convert to LF endings for consistency in mixing templates */ utils_ensure_same_eol_characters(str, SC_EOL_LF); return g_string_free(str, FALSE); } static void read_template(const gchar *name, gint id) { gchar *fname = g_build_path(G_DIR_SEPARATOR_S, app->configdir, GEANY_TEMPLATES_SUBDIR, name, NULL); /* try system if user template doesn't exist */ if (!g_file_test(fname, G_FILE_TEST_EXISTS)) setptr(fname, g_build_path(G_DIR_SEPARATOR_S, app->datadir, GEANY_TEMPLATES_SUBDIR, name, NULL)); templates[id] = read_file(fname); g_free(fname); } /* called when inserting templates into an existing document */ static void convert_eol_characters(GString *template, GeanyDocument *doc) { gint doc_eol_mode; if (doc == NULL) doc = document_get_current(); g_return_if_fail(doc != NULL); doc_eol_mode = editor_get_eol_char_mode(doc->editor); utils_ensure_same_eol_characters(template, doc_eol_mode); } static void init_general_templates(void) { /* read the contents */ read_template("fileheader", GEANY_TEMPLATE_FILEHEADER); read_template("gpl", GEANY_TEMPLATE_GPL); read_template("bsd", GEANY_TEMPLATE_BSD); read_template("function", GEANY_TEMPLATE_FUNCTION); read_template("changelog", GEANY_TEMPLATE_CHANGELOG); } void templates_replace_common(GString *template, const gchar *fname, GeanyFiletype *ft, const gchar *func_name) { gchar *shortname; if (fname == NULL) { if (!ft->extension) shortname = g_strdup(GEANY_STRING_UNTITLED); else shortname = g_strconcat(GEANY_STRING_UNTITLED, ".", ft->extension, NULL); } else shortname = g_path_get_basename(fname); templates_replace_valist(template, "{filename}", shortname, "{project}", app->project ? app->project->name : "", "{description}", app->project ? app->project->description : "", NULL); g_free(shortname); templates_replace_default_dates(template); templates_replace_command(template, fname, ft->name, func_name); /* Bug: command results could have {ob} {cb} strings in! */ /* replace braces last */ templates_replace_valist(template, "{ob}", "{", "{cb}", "}", NULL); } static gchar *get_template_from_file(const gchar *locale_fname, const gchar *doc_filename, GeanyFiletype *ft) { gchar *content; GString *template = NULL; content = read_file(locale_fname); if (content != NULL) { gchar *file_header; template = g_string_new(content); file_header = get_template_fileheader(ft); templates_replace_valist(template, "{fileheader}", file_header, NULL); templates_replace_common(template, doc_filename, ft, NULL); utils_free_pointers(2, file_header, content, NULL); return g_string_free(template, FALSE); } return NULL; } static void on_new_with_file_template(GtkMenuItem *menuitem, G_GNUC_UNUSED gpointer user_data) { gchar *fname = ui_menu_item_get_text(menuitem); GeanyFiletype *ft; gchar *template; const gchar *extension = strrchr(fname, '.'); /* easy way to get the file extension */ gchar *new_filename = g_strconcat(GEANY_STRING_UNTITLED, extension, NULL); gchar *path; ft = filetypes_detect_from_extension(fname); setptr(fname, utils_get_locale_from_utf8(fname)); /* fname is just the basename from the menu item, so prepend the custom files path */ path = g_build_path(G_DIR_SEPARATOR_S, app->configdir, GEANY_TEMPLATES_SUBDIR, "files", fname, NULL); template = get_template_from_file(path, new_filename, ft); if (!template) { /* try the system path */ g_free(path); path = g_build_path(G_DIR_SEPARATOR_S, app->datadir, GEANY_TEMPLATES_SUBDIR, "files", fname, NULL); template = get_template_from_file(path, new_filename, ft); } if (template) { /* line endings will be converted */ document_new_file(new_filename, ft, template); } else { setptr(fname, utils_get_utf8_from_locale(fname)); ui_set_statusbar(TRUE, _("Could not find file '%s'."), fname); } g_free(template); g_free(path); g_free(new_filename); g_free(fname); } static void add_file_item(const gchar *fname, GtkWidget *menu) { GtkWidget *tmp_button; gchar *label; g_return_if_fail(fname); g_return_if_fail(menu); label = utils_get_utf8_from_locale(fname); tmp_button = gtk_menu_item_new_with_label(label); gtk_widget_show(tmp_button); gtk_container_add(GTK_CONTAINER(menu), tmp_button); g_signal_connect(tmp_button, "activate", G_CALLBACK(on_new_with_file_template), NULL); g_free(label); } static gboolean add_custom_template_items(void) { GSList *list = utils_get_config_files(GEANY_TEMPLATES_SUBDIR G_DIR_SEPARATOR_S "files"); GSList *node; foreach_slist(node, list) { gchar *fname = node->data; add_file_item(fname, new_with_template_menu); g_free(fname); } g_slist_free(list); return list != NULL; } static void create_file_template_menu(void) { new_with_template_menu = gtk_menu_new(); add_custom_template_items(); /* unless the file menu is showing, menu should be in the toolbar widget */ geany_menu_button_action_set_menu(GEANY_MENU_BUTTON_ACTION( toolbar_get_action_by_name("New")), new_with_template_menu); } static void on_file_menu_show(GtkWidget *item) { geany_menu_button_action_set_menu( GEANY_MENU_BUTTON_ACTION(toolbar_get_action_by_name("New")), NULL); item = ui_lookup_widget(main_widgets.window, "menu_new_with_template1"); gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), new_with_template_menu); } static void on_file_menu_hide(GtkWidget *item) { item = ui_lookup_widget(main_widgets.window, "menu_new_with_template1"); gtk_menu_item_set_submenu(GTK_MENU_ITEM(item), NULL); geany_menu_button_action_set_menu( GEANY_MENU_BUTTON_ACTION(toolbar_get_action_by_name("New")), new_with_template_menu); } /* reload templates if any file in the templates path is saved */ static void on_document_save(G_GNUC_UNUSED GObject *object, GeanyDocument *doc) { gchar *path = utils_build_path(app->configdir, GEANY_TEMPLATES_SUBDIR, NULL); g_return_if_fail(NZV(doc->real_path)); if (strncmp(doc->real_path, path, strlen(path)) == 0) { /* reload templates */ templates_free_templates(); templates_init(); } g_free(path); } /* warning: also called when reloading template settings */ void templates_init(void) { static gboolean init_done = FALSE; init_general_templates(); create_file_template_menu(); /* we hold our own ref for the menu as it has no parent whilst being moved */ g_object_ref(new_with_template_menu); /* only connect signals to persistent objects once */ if (!init_done) { GtkWidget *item; /* reparent the template menu as needed */ item = ui_lookup_widget(main_widgets.window, "file1"); item = gtk_menu_item_get_submenu(GTK_MENU_ITEM(item)); g_signal_connect(item, "show", G_CALLBACK(on_file_menu_show), NULL); g_signal_connect(item, "hide", G_CALLBACK(on_file_menu_hide), NULL); g_signal_connect(geany_object, "document-save", G_CALLBACK(on_document_save), NULL); } init_done = TRUE; } /* indent is used to make some whitespace between comment char and real start of the line * e.g. indent = 8 prints " * here comes the text of the line" * indent is meant to be the whole amount of characters before the real line content follows, i.e. * 6 characters are filled with whitespace when the comment characters include " *" */ static void make_comment_block(GString *comment_text, gint filetype_idx, guint indent) { gchar *frame_start; /* to add before comment_text */ gchar *frame_end; /* to add after comment_text */ const gchar *line_prefix; /* to add before every line in comment_text */ gchar *tmp; gchar *prefix; gchar **lines; guint i, len; gint template_eol_mode; const gchar *template_eol_char; GeanyFiletype *ft = filetypes_index(filetype_idx); const gchar *co; const gchar *cc; g_return_if_fail(comment_text != NULL); g_return_if_fail(ft != NULL); template_eol_mode = utils_get_line_endings(comment_text->str, comment_text->len); template_eol_char = utils_get_eol_char(template_eol_mode); co = ft->comment_open; cc = NULL; if (NZV(co)) cc = ft->comment_close; else co = ft->comment_single; if (NZV(co)) { if (NZV(cc)) { frame_start = g_strconcat(co, template_eol_char, NULL); frame_end = g_strconcat(cc, template_eol_char, NULL); line_prefix = ""; } else { frame_start = NULL; frame_end = NULL; line_prefix = co; } } else { /* use C-like multi-line comments as fallback */ frame_start = g_strconcat("/*", template_eol_char, NULL); frame_end = g_strconcat("*/", template_eol_char, NULL); line_prefix = ""; } /* do some magic to nicely format C-like multi-line comments */ if (NZV(frame_start) && frame_start[1] == '*') { /* prefix the string with a space */ setptr(frame_end, g_strconcat(" ", frame_end, NULL)); line_prefix = " *"; } /* construct the real prefix with given amount of whitespace */ i = (indent > strlen(line_prefix)) ? (indent - strlen(line_prefix)) : strlen(line_prefix); tmp = g_strnfill(i, ' '); prefix = g_strconcat(line_prefix, tmp, NULL); g_free(tmp); /* add line_prefix to every line of comment_text */ lines = g_strsplit(comment_text->str, template_eol_char, -1); len = g_strv_length(lines); if (len > 0) /* prevent unsigned wraparound if comment_text is empty */ { for (i = 0; i < len - 1; i++) { tmp = lines[i]; lines[i] = g_strconcat(prefix, tmp, NULL); g_free(tmp); } } tmp = g_strjoinv(template_eol_char, lines); /* clear old contents */ g_string_erase(comment_text, 0, -1); /* add frame_end */ if (frame_start != NULL) g_string_append(comment_text, frame_start); /* add the new main content */ g_string_append(comment_text, tmp); /* add frame_start */ if (frame_end != NULL) g_string_append(comment_text, frame_end); utils_free_pointers(4, prefix, tmp, frame_start, frame_end, NULL); g_strfreev(lines); } gchar *templates_get_template_licence(GeanyDocument *doc, gint licence_type) { GString *template; g_return_val_if_fail(doc != NULL, NULL); g_return_val_if_fail(licence_type == GEANY_TEMPLATE_GPL || licence_type == GEANY_TEMPLATE_BSD, NULL); template = g_string_new(templates[licence_type]); replace_static_values(template); templates_replace_default_dates(template); templates_replace_command(template, DOC_FILENAME(doc), doc->file_type->name, NULL); make_comment_block(template, doc->file_type->id, GEANY_TEMPLATES_INDENT); convert_eol_characters(template, doc); return g_string_free(template, FALSE); } static gchar *get_template_fileheader(GeanyFiletype *ft) { GString *template = g_string_new(templates[GEANY_TEMPLATE_FILEHEADER]); filetypes_load_config(ft->id, FALSE); /* load any user extension setting */ templates_replace_valist(template, "{gpl}", templates[GEANY_TEMPLATE_GPL], "{bsd}", templates[GEANY_TEMPLATE_BSD], NULL); /* we don't replace other wildcards here otherwise they would get done twice for files */ make_comment_block(template, ft->id, GEANY_TEMPLATES_INDENT); return g_string_free(template, FALSE); } /* TODO change the signature to take a GeanyDocument? this would break plugin API/ABI */ gchar *templates_get_template_fileheader(gint filetype_idx, const gchar *fname) { GeanyFiletype *ft = filetypes[filetype_idx]; gchar *str = get_template_fileheader(ft); GString *template = g_string_new(str); g_free(str); templates_replace_common(template, fname, ft, NULL); convert_eol_characters(template, NULL); return g_string_free(template, FALSE); } gchar *templates_get_template_function(GeanyDocument *doc, const gchar *func_name) { GString *text; func_name = (func_name != NULL) ? func_name : ""; text = g_string_new(templates[GEANY_TEMPLATE_FUNCTION]); templates_replace_valist(text, "{functionname}", func_name, NULL); templates_replace_default_dates(text); templates_replace_command(text, DOC_FILENAME(doc), doc->file_type->name, func_name); make_comment_block(text, doc->file_type->id, GEANY_TEMPLATES_INDENT); convert_eol_characters(text, doc); return g_string_free(text, FALSE); } gchar *templates_get_template_changelog(GeanyDocument *doc) { GString *result = g_string_new(templates[GEANY_TEMPLATE_CHANGELOG]); const gchar *file_type_name = (doc != NULL) ? doc->file_type->name : ""; replace_static_values(result); templates_replace_default_dates(result); templates_replace_command(result, DOC_FILENAME(doc), file_type_name, NULL); convert_eol_characters(result, doc); return g_string_free(result, FALSE); } void templates_free_templates(void) { gint i; GList *children, *item; /* disconnect the menu from the action widget, so destroying the items below doesn't * trigger rebuilding of the menu on each item destroy */ geany_menu_button_action_set_menu( GEANY_MENU_BUTTON_ACTION(toolbar_get_action_by_name("New")), NULL); for (i = 0; i < GEANY_MAX_TEMPLATES; i++) { g_free(templates[i]); } /* destroy "New with template" sub menu items (in case we want to reload the templates) */ children = gtk_container_get_children(GTK_CONTAINER(new_with_template_menu)); foreach_list(item, children) { gtk_widget_destroy(GTK_WIDGET(item->data)); } g_list_free(children); g_object_unref(new_with_template_menu); new_with_template_menu = NULL; } static void replace_static_values(GString *text) { utils_string_replace_all(text, "{version}", template_prefs.version); utils_string_replace_all(text, "{initial}", template_prefs.initials); utils_string_replace_all(text, "{developer}", template_prefs.developer); utils_string_replace_all(text, "{mail}", template_prefs.mail); utils_string_replace_all(text, "{company}", template_prefs.company); utils_string_replace_all(text, "{untitled}", GEANY_STRING_UNTITLED); utils_string_replace_all(text, "{geanyversion}", "Geany " VERSION); } /* Replaces all static template wildcards (version, mail, company, name, ...) * plus those wildcard, value pairs which are passed, e.g. * * templates_replace_valist(text, "{some_wildcard}", "some value", * "{another_wildcard}", "another value", NULL); * * The argument list must be terminated with NULL. */ void templates_replace_valist(GString *text, const gchar *first_wildcard, ...) { va_list args; const gchar *key, *value; g_return_if_fail(text != NULL); va_start(args, first_wildcard); key = first_wildcard; value = va_arg(args, gchar*); while (key != NULL) { utils_string_replace_all(text, key, value); key = va_arg(args, gchar*); if (key == NULL || text == NULL) break; value = va_arg(args, gchar*); } va_end(args); replace_static_values(text); } static void templates_replace_default_dates(GString *text) { gchar *year = utils_get_date_time(template_prefs.year_format, NULL); gchar *date = utils_get_date_time(template_prefs.date_format, NULL); gchar *datetime = utils_get_date_time(template_prefs.datetime_format, NULL); g_return_if_fail(text != NULL); templates_replace_valist(text, "{year}", year, "{date}", date, "{datetime}", datetime, NULL); utils_free_pointers(3, year, date, datetime, NULL); } static gchar *run_command(const gchar *command, const gchar *file_name, const gchar *file_type, const gchar *func_name) { gchar *result = NULL; gchar **argv; if (g_shell_parse_argv(command, NULL, &argv, NULL)) { GError *error = NULL; gchar **env; file_name = (file_name != NULL) ? file_name : ""; file_type = (file_type != NULL) ? file_type : ""; func_name = (func_name != NULL) ? func_name : ""; env = utils_copy_environment(NULL, "GEANY_FILENAME", file_name, "GEANY_FILETYPE", file_type, "GEANY_FUNCNAME", func_name, NULL); if (! utils_spawn_sync(NULL, argv, env, G_SPAWN_SEARCH_PATH, NULL, NULL, &result, NULL, NULL, &error)) { g_warning("templates_replace_command: %s", error->message); g_error_free(error); return NULL; } g_strfreev(argv); g_strfreev(env); } return result; } static void templates_replace_command(GString *text, const gchar *file_name, const gchar *file_type, const gchar *func_name) { gchar *match = NULL; gchar *wildcard = NULL; gchar *cmd; gchar *result; g_return_if_fail(text != NULL); while ((match = strstr(text->str, "{command:")) != NULL) { cmd = match; while (*match != '}' && *match != '\0') match++; wildcard = g_strndup(cmd, match - cmd + 1); cmd = g_strndup(wildcard + 9, strlen(wildcard) - 10); result = run_command(cmd, file_name, file_type, func_name); if (result != NULL) { result = g_strstrip(result); utils_string_replace_first(text, wildcard, result); g_free(result); } else utils_string_replace_first(text, wildcard, ""); g_free(wildcard); g_free(cmd); } }