/* ide-snippet.c * * Copyright 2013-2019 Christian Hergert * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General 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 "ide-snippet" #include "config.h" #include #include #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_FINAL_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_get_full_text: * @self: a #IdeSnippet * * Gets the contents of the snippet as currently edited by the user. * * Returns: (transfer full): a newly allocated string containing the full * contents of all the snippet chunks. * * Since: 3.32 */ gchar * ide_snippet_get_full_text (IdeSnippet *self) { GtkTextIter begin; GtkTextIter end; g_return_val_if_fail (IDE_IS_SNIPPET (self), NULL); if (self->mark_begin == NULL || self->mark_end == NULL) return NULL; 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); return gtk_text_iter_get_slice (&begin, &end); } /** * ide_snippet_replace_current_chunk_text: * @self: a #IdeSnippet * @new_text: the text to use as the replacement * * This replaces the current chunk (if any) to contain the contents of * @new_text. * * This function is primarily useful to the #IdeSourceView as it updates * content as the user types. * * Since: 3.32 */ void ide_snippet_replace_current_chunk_text (IdeSnippet *self, const gchar *new_text) { IdeSnippetChunk *chunk; gint utf8_len; g_return_if_fail (IDE_IS_SNIPPET (self)); g_return_if_fail (self->chunks != NULL); if (self->current_chunk < 0 || self->current_chunk >= self->chunks->len) return; chunk = g_ptr_array_index (self->chunks, self->current_chunk); ide_snippet_chunk_set_text (chunk, new_text); ide_snippet_chunk_set_text_set (chunk, TRUE); g_assert (self->current_chunk >= 0); g_assert (self->current_chunk < self->runs->len); utf8_len = g_utf8_strlen (new_text, -1); g_array_index (self->runs, gint, self->current_chunk) = utf8_len; } /** * 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; } }