1 /*
2  * pluma-bookmarks-plugin.c - Bookmarking for pluma
3  *
4  * Copyright (C) 2008 Jesse van den Kieboom
5  * Copyright (C) 2020-2021 MATE Developers
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2, or (at your option)
10  * any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  */
21 
22 #ifdef HAVE_CONFIG_H
23 #include <config.h>
24 #endif
25 
26 #include "pluma-bookmarks-plugin.h"
27 
28 #include <stdlib.h>
29 #include <glib/gi18n-lib.h>
30 #include <gtk/gtk.h>
31 #include <gmodule.h>
32 #include <gtksourceview/gtksource.h>
33 
34 #include <pluma/pluma-window-activatable.h>
35 #include <pluma/pluma-debug.h>
36 #include <pluma/pluma-window.h>
37 #include <pluma/pluma-document.h>
38 
39 #define BOOKMARK_CATEGORY "PlumaBookmarksPluginBookmark"
40 #define BOOKMARK_PRIORITY 1
41 
42 #define INSERT_DATA_KEY "PlumaBookmarksInsertData"
43 #define METADATA_ATTR "metadata::pluma-bookmarks"
44 
45 #define MESSAGE_OBJECT_PATH "/plugins/bookmarks"
46 #define BUS_CONNECT(bus, name, data) pluma_message_bus_connect(bus, MESSAGE_OBJECT_PATH, #name, (PlumaMessageCallback)  message_##name##_cb, data, NULL)
47 
48 typedef struct
49 {
50 	GtkSourceMark  *bookmark;
51 	GtkTextMark    *mark;
52 } InsertTracker;
53 
54 typedef struct
55 {
56 	GSList  *trackers;
57 	guint    user_action;
58 } InsertData;
59 
60 static void on_style_scheme_notify		(GObject     *object,
61                                                  GParamSpec  *pspec,
62                                                  PlumaView   *view);
63 
64 static void on_delete_range			(GtkTextBuffer *buffer,
65 						 GtkTextIter   *start,
66 						 GtkTextIter   *end,
67 						 gpointer       user_data);
68 
69 static void on_insert_text_before		(GtkTextBuffer *buffer,
70                                                  GtkTextIter   *location,
71                                                  gchar         *text,
72                                                  gint		len,
73                                                  InsertData    *data);
74 
75 static void on_begin_user_action		(GtkTextBuffer *buffer,
76                                                  InsertData    *data);
77 
78 static void on_end_user_action                  (GtkTextBuffer *buffer,
79                                                  InsertData    *data);
80 
81 static void on_toggle_bookmark_activate         (GtkAction            *action,
82                                                  PlumaBookmarksPlugin *plugin);
83 static void on_next_bookmark_activate           (GtkAction            *action,
84                                                  PlumaBookmarksPlugin *plugin);
85 static void on_previous_bookmark_activate 	(GtkAction            *action,
86                                                  PlumaBookmarksPlugin *plugin);
87 static void on_tab_added 			(PlumaWindow          *window,
88                                                  PlumaTab             *tab,
89                                                  PlumaBookmarksPlugin *plugin);
90 static void on_tab_removed 			(PlumaWindow          *window,
91                                                  PlumaTab             *tab,
92                                                  PlumaBookmarksPlugin *plugin);
93 
94 static void add_bookmark    (GtkSourceBuffer *buffer, GtkTextIter *iter);
95 static void remove_bookmark (GtkSourceBuffer *buffer, GtkTextIter *iter);
96 static void toggle_bookmark (GtkSourceBuffer *buffer, GtkTextIter *iter);
97 
98 static void pluma_window_activatable_iface_init (PlumaWindowActivatableInterface *iface);
99 
100 struct _PlumaBookmarksPluginPrivate
101 {
102 	PlumaWindow     *window;
103 
104 	GtkActionGroup  *action_group;
105 	guint            ui_id;
106 };
107 
108 G_DEFINE_DYNAMIC_TYPE_EXTENDED (PlumaBookmarksPlugin,
109                                 pluma_bookmarks_plugin,
110                                 PEAS_TYPE_EXTENSION_BASE,
111                                 0,
112                                 G_ADD_PRIVATE_DYNAMIC (PlumaBookmarksPlugin)
113                                 G_IMPLEMENT_INTERFACE_DYNAMIC (PLUMA_TYPE_WINDOW_ACTIVATABLE,
114                                                                pluma_window_activatable_iface_init))
115 
116 enum
117 {
118 	PROP_0,
119 	PROP_WINDOW
120 };
121 
122 static void
pluma_bookmarks_plugin_init(PlumaBookmarksPlugin * plugin)123 pluma_bookmarks_plugin_init (PlumaBookmarksPlugin *plugin)
124 {
125 	pluma_debug_message (DEBUG_PLUGINS, "PlumaBookmarksPlugin initializing");
126 
127 	plugin->priv = pluma_bookmarks_plugin_get_instance_private (plugin);
128 }
129 
130 static void
pluma_bookmarks_plugin_dispose(GObject * object)131 pluma_bookmarks_plugin_dispose (GObject *object)
132 {
133 	PlumaBookmarksPlugin *plugin = PLUMA_BOOKMARKS_PLUGIN (object);
134 
135 	pluma_debug_message (DEBUG_PLUGINS, "PlumaBookmarksPlugin disposing");
136 
137 	if (plugin->priv->action_group != NULL)
138 	{
139 		g_object_unref (plugin->priv->action_group);
140 		plugin->priv->action_group = NULL;
141 	}
142 
143 	if (plugin->priv->window != NULL)
144 	{
145 		g_object_unref (plugin->priv->window);
146 		plugin->priv->window = NULL;
147 	}
148 
149 	G_OBJECT_CLASS (pluma_bookmarks_plugin_parent_class)->dispose (object);
150 }
151 
152 static void
pluma_bookmarks_plugin_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)153 pluma_bookmarks_plugin_set_property (GObject      *object,
154                                      guint         prop_id,
155                                      const GValue *value,
156                                      GParamSpec   *pspec)
157 {
158 	PlumaBookmarksPlugin *plugin = PLUMA_BOOKMARKS_PLUGIN (object);
159 
160 	switch (prop_id)
161 	{
162 		case PROP_WINDOW:
163 			plugin->priv->window = PLUMA_WINDOW (g_value_dup_object (value));
164 			break;
165 
166 		default:
167 			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
168 			break;
169 	}
170 }
171 
172 static void
pluma_bookmarks_plugin_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)173 pluma_bookmarks_plugin_get_property (GObject    *object,
174                                      guint       prop_id,
175                                      GValue     *value,
176                                      GParamSpec *pspec)
177 {
178 	PlumaBookmarksPlugin *plugin = PLUMA_BOOKMARKS_PLUGIN (object);
179 
180 	switch (prop_id)
181 	{
182 		case PROP_WINDOW:
183 			g_value_set_object (value, plugin->priv->window);
184 			break;
185 
186 		default:
187 			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
188 			break;
189 	}
190 }
191 
192 static void
free_insert_data(InsertData * data)193 free_insert_data (InsertData *data)
194 {
195 	g_slice_free (InsertData, data);
196 }
197 
198 static GtkActionEntry const action_entries[] = {
199 	{"ToggleBookmark", NULL, N_("Toggle Bookmark"), "<Control><Alt>B",
200 	 N_("Toggle bookmark status of the current line"),
201 	 G_CALLBACK (on_toggle_bookmark_activate)},
202 	{"NextBookmark", NULL, N_("Go to Next Bookmark"), "<Control>B",
203 	 N_("Go to the next bookmark"),
204 	 G_CALLBACK (on_next_bookmark_activate)},
205 	{"PreviousBookmark", NULL, N_("Go to Previous Bookmark"), "<Control><Shift>B",
206 	 N_("Go to the previous bookmark"),
207 	 G_CALLBACK (on_previous_bookmark_activate)}
208 };
209 
210 static gchar const uidefinition[] = ""
211 "<ui>"
212 "  <menubar name='MenuBar'>"
213 "    <menu name='EditMenu' action='Edit'>"
214 "      <placeholder name='EditOps_6'>"
215 "        <menuitem action='ToggleBookmark'/>"
216 "        <menuitem action='PreviousBookmark'/>"
217 "        <menuitem action='NextBookmark'/>"
218 "      </placeholder>"
219 "    </menu>"
220 "  </menubar>"
221 "</ui>";
222 
223 static void
install_menu(PlumaBookmarksPlugin * plugin)224 install_menu (PlumaBookmarksPlugin *plugin)
225 {
226 	PlumaBookmarksPluginPrivate *priv;
227 	GtkUIManager                *manager;
228 	GError                      *error = NULL;
229 
230 	priv = plugin->priv;
231 	manager = pluma_window_get_ui_manager (priv->window);
232 	priv->action_group = gtk_action_group_new ("PlumaBookmarksPluginActions");
233 
234 	gtk_action_group_set_translation_domain (priv->action_group,
235 						 GETTEXT_PACKAGE);
236 
237 	gtk_action_group_add_actions (priv->action_group,
238 				      action_entries,
239 				      G_N_ELEMENTS (action_entries),
240 				      plugin);
241 
242 	gtk_ui_manager_insert_action_group (manager, priv->action_group, -1);
243 	priv->ui_id = gtk_ui_manager_add_ui_from_string (manager, uidefinition, -1, &error);
244 
245 	if (!priv->ui_id)
246 	{
247 		g_warning ("Could not load UI: %s", error->message);
248 		g_error_free (error);
249 	}
250 }
251 
252 static void
uninstall_menu(PlumaBookmarksPlugin * plugin)253 uninstall_menu (PlumaBookmarksPlugin *plugin)
254 {
255 	PlumaBookmarksPluginPrivate *priv;
256 	GtkUIManager                *manager;
257 
258 	priv = plugin->priv;
259 	manager = pluma_window_get_ui_manager (priv->window);
260 
261 	gtk_ui_manager_remove_ui (manager, priv->ui_id);
262 	gtk_ui_manager_remove_action_group (manager, priv->action_group);
263 
264 	g_object_unref (priv->action_group);
265 	priv->action_group = NULL;
266 }
267 
268 static void
remove_all_bookmarks(GtkSourceBuffer * buffer)269 remove_all_bookmarks (GtkSourceBuffer *buffer)
270 {
271 	GtkTextIter start;
272 	GtkTextIter end;
273 
274 	pluma_debug (DEBUG_PLUGINS);
275 
276 	gtk_text_buffer_get_bounds (GTK_TEXT_BUFFER (buffer), &start, &end);
277 	gtk_source_buffer_remove_source_marks (buffer,
278 					       &start,
279 					       &end,
280 					       BOOKMARK_CATEGORY);
281 }
282 
283 static void
disable_bookmarks(PlumaView * view)284 disable_bookmarks (PlumaView *view)
285 {
286 	GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
287 	gpointer data;
288 
289 	gtk_source_view_set_show_line_marks (GTK_SOURCE_VIEW (view), FALSE);
290 	remove_all_bookmarks (GTK_SOURCE_BUFFER (buffer));
291 
292 	g_signal_handlers_disconnect_by_func (buffer, on_style_scheme_notify, view);
293 	g_signal_handlers_disconnect_by_func (buffer, on_delete_range, NULL);
294 
295 	data = g_object_get_data (G_OBJECT (buffer), INSERT_DATA_KEY);
296 
297 	g_signal_handlers_disconnect_by_func (buffer, on_insert_text_before, data);
298 	g_signal_handlers_disconnect_by_func (buffer, on_begin_user_action, data);
299 	g_signal_handlers_disconnect_by_func (buffer, on_end_user_action, data);
300 
301 	g_object_set_data (G_OBJECT (buffer), INSERT_DATA_KEY, NULL);
302 }
303 
304 static GdkPixbuf *
get_bookmark_pixbuf(PlumaBookmarksPlugin * plugin)305 get_bookmark_pixbuf (PlumaBookmarksPlugin *plugin)
306 {
307 	GdkPixbuf *pixbuf;
308 	gint       width;
309 	GError    *error = NULL;
310 
311 	gtk_icon_size_lookup (GTK_ICON_SIZE_MENU, &width, NULL);
312 	pixbuf = gtk_icon_theme_load_icon (gtk_icon_theme_get_default (),
313 	                                   "user-bookmarks-symbolic",
314 	                                   (width * 2) / 3,
315 	                                   0,
316 	                                   &error);
317 
318 	if (error != NULL)
319 	{
320 		g_warning ("Could not load theme icon user-bookmarks-symbolic: %s",
321 		           error->message);
322 		g_error_free (error);
323 	}
324 
325 	return pixbuf;
326 }
327 
328 static void
update_background_color(GtkSourceMarkAttributes * attrs,GtkSourceBuffer * buffer)329 update_background_color (GtkSourceMarkAttributes *attrs,
330                          GtkSourceBuffer         *buffer)
331 {
332 	GtkSourceStyleScheme *scheme;
333 	GtkSourceStyle       *style;
334 
335 	scheme = gtk_source_buffer_get_style_scheme (buffer);
336 	style = gtk_source_style_scheme_get_style (scheme, "search-match");
337 
338 	if (style)
339 	{
340 		gboolean bgset;
341 		gchar *bg;
342 
343 		g_object_get (style, "background-set", &bgset, "background", &bg, NULL);
344 
345 		if (bgset)
346 		{
347 			GdkRGBA color;
348 
349 			gdk_rgba_parse (&color, bg);
350 			gtk_source_mark_attributes_set_background (attrs, &color);
351 			g_free (bg);
352 
353 			return;
354 		}
355 	}
356 
357 	gtk_source_mark_attributes_set_background (attrs, NULL);
358 }
359 
360 static void
enable_bookmarks(PlumaView * view,PlumaBookmarksPlugin * plugin)361 enable_bookmarks (PlumaView            *view,
362                   PlumaBookmarksPlugin *plugin)
363 {
364 	GdkPixbuf *pixbuf;
365 
366 	pixbuf = get_bookmark_pixbuf (plugin);
367 
368 	/* Make sure the category pixbuf is set */
369 	if (pixbuf)
370 	{
371 		GtkTextBuffer *buffer;
372 		GtkSourceMarkAttributes *attrs;
373 		InsertData *data;
374 
375 		buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
376 
377 		attrs = gtk_source_mark_attributes_new ();
378 
379 		update_background_color (attrs, GTK_SOURCE_BUFFER (buffer));
380 		gtk_source_mark_attributes_set_pixbuf (attrs, pixbuf);
381 		g_object_unref (pixbuf);
382 
383 		gtk_source_view_set_mark_attributes (GTK_SOURCE_VIEW (view),
384 						     BOOKMARK_CATEGORY,
385 						     attrs,
386 						     BOOKMARK_PRIORITY);
387 
388 		gtk_source_view_set_show_line_marks (GTK_SOURCE_VIEW (view), TRUE);
389 
390 		g_signal_connect (buffer,
391 				  "notify::style-scheme",
392 				  G_CALLBACK (on_style_scheme_notify),
393 				  view);
394 
395 		g_signal_connect (buffer,
396 				  "delete-range",
397 				  G_CALLBACK (on_delete_range),
398 				  NULL);
399 
400 		data = g_slice_new0 (InsertData);
401 
402 		g_object_set_data_full (G_OBJECT (buffer),
403 					INSERT_DATA_KEY,
404 					data,
405 					(GDestroyNotify) free_insert_data);
406 
407 		g_signal_connect (buffer,
408 				  "insert-text",
409 				  G_CALLBACK (on_insert_text_before),
410 				  data);
411 
412 		g_signal_connect (buffer,
413 				  "begin-user-action",
414 				  G_CALLBACK (on_begin_user_action),
415 				  data);
416 
417 		g_signal_connect (buffer,
418 				  "end-user-action",
419 				  G_CALLBACK (on_end_user_action),
420 				  data);
421 
422 	}
423 	else
424 	{
425 		g_warning ("Could not set bookmark icon!");
426 	}
427 }
428 
429 static void
load_bookmarks(PlumaView * view,gchar ** bookmarks)430 load_bookmarks (PlumaView  *view,
431                 gchar     **bookmarks)
432 {
433 	GtkSourceBuffer *buf;
434 	GtkTextIter      iter;
435 	gint             tot_lines;
436 	gint             i;
437 
438 	pluma_debug (DEBUG_PLUGINS);
439 
440 	buf = GTK_SOURCE_BUFFER (gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)));
441 
442 	gtk_text_buffer_get_end_iter (GTK_TEXT_BUFFER (buf), &iter);
443 	tot_lines = gtk_text_iter_get_line (&iter);
444 
445 	for (i = 0; bookmarks != NULL && bookmarks[i] != NULL; i++)
446 	{
447 		gint line;
448 
449 		line = atoi (bookmarks[i]);
450 
451 		if (line >= 0 && line < tot_lines)
452 		{
453 			GSList *marks;
454 
455 			gtk_text_buffer_get_iter_at_line (GTK_TEXT_BUFFER (buf),
456 							  &iter, line);
457 
458 			marks = gtk_source_buffer_get_source_marks_at_iter (buf, &iter,
459 									    BOOKMARK_CATEGORY);
460 			if (marks == NULL)
461 				/* Add new bookmark */
462 				gtk_source_buffer_create_source_mark (buf,
463 								      NULL,
464 								      BOOKMARK_CATEGORY,
465 								      &iter);
466 			else
467 				g_slist_free (marks);
468 		}
469 	}
470 }
471 
472 static void
load_bookmark_metadata(PlumaView * view)473 load_bookmark_metadata (PlumaView *view)
474 {
475 	PlumaDocument *doc;
476 	gchar         *bookmarks_attr;
477 
478 	doc = PLUMA_DOCUMENT (gtk_text_view_get_buffer (GTK_TEXT_VIEW (view)));
479 	bookmarks_attr = pluma_document_get_metadata (doc, METADATA_ATTR);
480 
481 	if (bookmarks_attr != NULL)
482 	{
483 		gchar **bookmarks;
484 
485 		bookmarks = g_strsplit (bookmarks_attr, ",", -1);
486 		g_free (bookmarks_attr);
487 
488 		load_bookmarks (view, bookmarks);
489 
490 		g_strfreev (bookmarks);
491 	}
492 }
493 
494 typedef gboolean (*IterSearchFunc)(GtkSourceBuffer *buffer, GtkTextIter *iter, const gchar *category);
495 typedef void (*CycleFunc)(GtkTextBuffer *buffer, GtkTextIter *iter);
496 
497 static void
goto_bookmark(PlumaWindow * window,GtkSourceView * view,GtkTextIter * iter,IterSearchFunc func,CycleFunc cycle_func)498 goto_bookmark (PlumaWindow    *window,
499                GtkSourceView  *view,
500                GtkTextIter    *iter,
501                IterSearchFunc  func,
502                CycleFunc       cycle_func)
503 {
504 	GtkTextBuffer *buffer;
505 	GtkTextIter    at;
506 	GtkTextIter    end;
507 
508 	if (view == NULL)
509 		view = GTK_SOURCE_VIEW (pluma_window_get_active_view (window));
510 
511 	g_return_if_fail (view != NULL);
512 
513 	buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
514 
515 	if (iter == NULL)
516 		gtk_text_buffer_get_iter_at_mark (buffer,
517 		                                  &at,
518 		                                  gtk_text_buffer_get_insert (buffer));
519 	else
520 		at = *iter;
521 
522 	/* Move the iter to the beginning of the line, where the bookmarks are */
523 	gtk_text_iter_set_line_offset (&at, 0);
524 
525 	/* Try to find the next bookmark */
526 	if (!func (GTK_SOURCE_BUFFER (buffer), &at, BOOKMARK_CATEGORY))
527 	{
528 		GSList *marks;
529 
530 		/* cycle through */
531 		cycle_func (buffer, &at);
532 		gtk_text_iter_set_line_offset (&at, 0);
533 
534 		marks = gtk_source_buffer_get_source_marks_at_iter (GTK_SOURCE_BUFFER (buffer),
535 		                                                    &at,
536 		                                                    BOOKMARK_CATEGORY);
537 
538 		if (!marks && !func (GTK_SOURCE_BUFFER (buffer), &at, BOOKMARK_CATEGORY))
539 			return;
540 
541 		g_slist_free (marks);
542 	}
543 
544 	end = at;
545 	if (!gtk_text_iter_forward_visible_line (&end))
546 		gtk_text_buffer_get_end_iter (buffer, &end);
547 	else
548 		gtk_text_iter_backward_char (&end);
549 
550 	gtk_text_buffer_select_range (buffer, &at, &end);
551 	gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (view), &at, 0.3, FALSE, 0, 0);
552 }
553 
554 static void
message_get_view_iter(PlumaWindow * window,PlumaMessage * message,GtkSourceView ** view,GtkTextIter * iter)555 message_get_view_iter (PlumaWindow    *window,
556                        PlumaMessage   *message,
557                        GtkSourceView **view,
558                        GtkTextIter    *iter)
559 {
560 	if (pluma_message_has_key (message, "view"))
561 		pluma_message_get (message, "view", view, NULL);
562 	else
563 		*view = GTK_SOURCE_VIEW (pluma_window_get_active_view (window));
564 
565 	g_return_if_fail (*view);
566 
567 	if (pluma_message_has_key (message, "iter"))
568 	{
569 		pluma_message_get (message, "iter", iter, NULL);
570 	}
571 	else
572 	{
573 		GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (*view));
574 		gtk_text_buffer_get_iter_at_mark (buffer,
575 		                                  iter,
576 		                                  gtk_text_buffer_get_insert (buffer));
577 	}
578 }
579 
580 static void
message_toggle_cb(PlumaMessageBus * bus,PlumaMessage * message,PlumaWindow * window)581 message_toggle_cb (PlumaMessageBus *bus,
582                    PlumaMessage    *message,
583                    PlumaWindow     *window)
584 {
585 	GtkSourceView *view = NULL;
586 	GtkTextIter iter;
587 
588 	message_get_view_iter (window, message, &view, &iter);
589 
590 	g_return_if_fail (view);
591 
592 	toggle_bookmark (GTK_SOURCE_BUFFER (gtk_text_view_get_buffer (GTK_TEXT_VIEW (view))),
593 	                 &iter);
594 }
595 
596 static void
message_add_cb(PlumaMessageBus * bus,PlumaMessage * message,PlumaWindow * window)597 message_add_cb (PlumaMessageBus *bus,
598                 PlumaMessage    *message,
599                 PlumaWindow     *window)
600 {
601 	GtkSourceView *view = NULL;
602 	GtkTextIter iter;
603 
604 	message_get_view_iter (window, message, &view, &iter);
605 
606 	g_return_if_fail (view);
607 
608 	add_bookmark (GTK_SOURCE_BUFFER (gtk_text_view_get_buffer (GTK_TEXT_VIEW (view))),
609 	              &iter);
610 }
611 
612 static void
message_remove_cb(PlumaMessageBus * bus,PlumaMessage * message,PlumaWindow * window)613 message_remove_cb (PlumaMessageBus *bus,
614                    PlumaMessage    *message,
615                    PlumaWindow     *window)
616 {
617 	GtkSourceView *view = NULL;
618 	GtkTextIter iter;
619 
620 	message_get_view_iter (window, message, &view, &iter);
621 
622 	g_return_if_fail (view);
623 
624 	remove_bookmark (GTK_SOURCE_BUFFER (gtk_text_view_get_buffer (GTK_TEXT_VIEW (view))),
625 	                 &iter);
626 }
627 
628 static void
message_goto_next_cb(PlumaMessageBus * bus,PlumaMessage * message,PlumaWindow * window)629 message_goto_next_cb (PlumaMessageBus *bus,
630                       PlumaMessage    *message,
631                       PlumaWindow     *window)
632 {
633 	GtkSourceView *view = NULL;
634 	GtkTextIter iter;
635 
636 	message_get_view_iter (window, message, &view, &iter);
637 
638 	g_return_if_fail (view);
639 
640 	goto_bookmark (window,
641 	               view,
642 	               &iter,
643 	               gtk_source_buffer_forward_iter_to_source_mark,
644 	               gtk_text_buffer_get_start_iter);
645 }
646 
647 static void
message_goto_previous_cb(PlumaMessageBus * bus,PlumaMessage * message,PlumaWindow * window)648 message_goto_previous_cb (PlumaMessageBus *bus,
649                           PlumaMessage    *message,
650                           PlumaWindow     *window)
651 {
652 	GtkSourceView *view = NULL;
653 	GtkTextIter iter;
654 
655 	message_get_view_iter (window, message, &view, &iter);
656 
657 	g_return_if_fail (view);
658 
659 	goto_bookmark (window,
660 	               view,
661 	               &iter,
662 	               gtk_source_buffer_backward_iter_to_source_mark,
663 	               gtk_text_buffer_get_end_iter);
664 }
665 
666 static void
install_messages(PlumaWindow * window)667 install_messages (PlumaWindow *window)
668 {
669 	PlumaMessageBus *bus = pluma_window_get_message_bus (window);
670 
671 	pluma_message_bus_register (bus,
672 	                            MESSAGE_OBJECT_PATH,
673 	                           "toggle",
674 	                            2,
675 	                            "view", GTK_SOURCE_TYPE_VIEW,
676 	                            "iter", GTK_TYPE_TEXT_ITER,
677 				    NULL);
678 
679 	pluma_message_bus_register (bus,
680 	                            MESSAGE_OBJECT_PATH,
681 	                            "add",
682 	                            2,
683 	                            "view", GTK_SOURCE_TYPE_VIEW,
684 	                            "iter", GTK_TYPE_TEXT_ITER,
685 				    NULL);
686 
687 	pluma_message_bus_register (bus,
688 	                            MESSAGE_OBJECT_PATH,
689 	                            "remove",
690 	                            2,
691 	                            "view", GTK_SOURCE_TYPE_VIEW,
692 	                            "iter", GTK_TYPE_TEXT_ITER,
693 				    NULL);
694 
695 	pluma_message_bus_register (bus,
696 	                            MESSAGE_OBJECT_PATH,
697 	                            "goto_next",
698 	                            2,
699 	                            "view", GTK_SOURCE_TYPE_VIEW,
700 	                            "iter", GTK_TYPE_TEXT_ITER,
701 				    NULL);
702 
703 	pluma_message_bus_register (bus,
704 	                            MESSAGE_OBJECT_PATH,
705 	                            "goto_previous",
706 	                            2,
707 	                            "view", GTK_SOURCE_TYPE_VIEW,
708 	                            "iter", GTK_TYPE_TEXT_ITER,
709 				    NULL);
710 
711 	BUS_CONNECT (bus, toggle, window);
712 	BUS_CONNECT (bus, add, window);
713 	BUS_CONNECT (bus, remove, window);
714 	BUS_CONNECT (bus, goto_next, window);
715 	BUS_CONNECT (bus, goto_previous, window);
716 }
717 
718 static void
uninstall_messages(PlumaWindow * window)719 uninstall_messages (PlumaWindow *window)
720 {
721 	PlumaMessageBus *bus = pluma_window_get_message_bus (window);
722 	pluma_message_bus_unregister_all (bus, MESSAGE_OBJECT_PATH);
723 }
724 
725 static void
pluma_bookmarks_plugin_activate(PlumaWindowActivatable * activatable)726 pluma_bookmarks_plugin_activate (PlumaWindowActivatable *activatable)
727 {
728 	PlumaBookmarksPluginPrivate *priv;
729 	GList *views;
730 	GList *item;
731 
732 	pluma_debug (DEBUG_PLUGINS);
733 
734 	priv = PLUMA_BOOKMARKS_PLUGIN (activatable)->priv;
735 
736 	views = pluma_window_get_views (priv->window);
737 	for (item = views; item != NULL; item = item->next)
738 	{
739 		enable_bookmarks (PLUMA_VIEW (item->data),
740 				  PLUMA_BOOKMARKS_PLUGIN (activatable));
741 		load_bookmark_metadata (PLUMA_VIEW (item->data));
742 	}
743 
744 	g_list_free (views);
745 
746 	g_signal_connect (priv->window, "tab-added",
747 			  G_CALLBACK (on_tab_added), activatable);
748 
749 	g_signal_connect (priv->window, "tab-removed",
750 			  G_CALLBACK (on_tab_removed), activatable);
751 
752 	install_menu (PLUMA_BOOKMARKS_PLUGIN (activatable));
753 	install_messages (priv->window);
754 }
755 
756 static void
pluma_bookmarks_plugin_update_state(PlumaWindowActivatable * activatable)757 pluma_bookmarks_plugin_update_state (PlumaWindowActivatable *activatable)
758 {
759 	PlumaBookmarksPluginPrivate *priv;
760 
761 	priv = PLUMA_BOOKMARKS_PLUGIN (activatable)->priv;
762 	gtk_action_group_set_sensitive (priv->action_group,
763 					pluma_window_get_active_view (priv->window) != NULL);
764 }
765 
766 static void
save_bookmark_metadata(PlumaView * view)767 save_bookmark_metadata (PlumaView *view)
768 {
769 	GtkTextIter    iter;
770 	GtkTextBuffer *buf;
771 	GString       *string;
772 	gchar         *val = NULL;
773 	gboolean       first = TRUE;
774 
775 	buf = gtk_text_view_get_buffer (GTK_TEXT_VIEW (view));
776 	gtk_text_buffer_get_start_iter (buf, &iter);
777 	string = g_string_new (NULL);
778 
779 	while (gtk_source_buffer_forward_iter_to_source_mark (GTK_SOURCE_BUFFER (buf),
780 							      &iter,
781 							      BOOKMARK_CATEGORY))
782 	{
783 		gint line;
784 
785 		line = gtk_text_iter_get_line (&iter);
786 
787 		if (!first)
788 		{
789 			g_string_append_printf (string, ",%d", line);
790 		}
791 		else
792 		{
793 			g_string_append_printf (string, "%d", line);
794 			first = FALSE;
795 		}
796 	}
797 
798 	if (string->len == 0)
799 	{
800 		val = g_string_free (string, TRUE);
801 		val = NULL;
802 	}
803 	else
804 	{
805 		val = g_string_free (string, FALSE);
806 	}
807 
808 	pluma_document_set_metadata (PLUMA_DOCUMENT (buf), METADATA_ATTR,
809 				     val, NULL);
810 
811 	g_free (val);
812 }
813 
814 static void
pluma_bookmarks_plugin_deactivate(PlumaWindowActivatable * activatable)815 pluma_bookmarks_plugin_deactivate (PlumaWindowActivatable *activatable)
816 {
817 	PlumaBookmarksPluginPrivate *priv;
818 	GList *views;
819 	GList *item;
820 
821 	pluma_debug (DEBUG_PLUGINS);
822 
823 	priv = PLUMA_BOOKMARKS_PLUGIN (activatable)->priv;
824 
825 	uninstall_menu (PLUMA_BOOKMARKS_PLUGIN (activatable));
826 	uninstall_messages (priv->window);
827 
828 	views = pluma_window_get_views (priv->window);
829 
830 	for (item = views; item != NULL; item = item->next)
831 		disable_bookmarks (PLUMA_VIEW (item->data));
832 
833 	g_list_free (views);
834 
835 	g_signal_handlers_disconnect_by_func (priv->window, on_tab_added, activatable);
836 	g_signal_handlers_disconnect_by_func (priv->window, on_tab_removed, activatable);
837 }
838 
839 static void
pluma_bookmarks_plugin_class_init(PlumaBookmarksPluginClass * klass)840 pluma_bookmarks_plugin_class_init (PlumaBookmarksPluginClass *klass)
841 {
842 	GObjectClass *object_class = G_OBJECT_CLASS (klass);
843 
844 	object_class->dispose      = pluma_bookmarks_plugin_dispose;
845 	object_class->set_property = pluma_bookmarks_plugin_set_property;
846 	object_class->get_property = pluma_bookmarks_plugin_get_property;
847 
848 	g_object_class_override_property (object_class, PROP_WINDOW, "window");
849 }
850 
851 static void
pluma_bookmarks_plugin_class_finalize(PlumaBookmarksPluginClass * klass)852 pluma_bookmarks_plugin_class_finalize (PlumaBookmarksPluginClass *klass)
853 {
854 }
855 
856 static void
on_style_scheme_notify(GObject * object,GParamSpec * pspec,PlumaView * view)857 on_style_scheme_notify (GObject     *object,
858 			GParamSpec  *pspec,
859 			PlumaView   *view)
860 {
861 	GtkSourceMarkAttributes *attrs;
862 
863 	attrs = gtk_source_view_get_mark_attributes (GTK_SOURCE_VIEW (view),
864 						     BOOKMARK_CATEGORY,
865 						     NULL);
866 
867 	update_background_color (attrs, GTK_SOURCE_BUFFER (object));
868 }
869 
870 static void
on_delete_range(GtkTextBuffer * buffer,GtkTextIter * start,GtkTextIter * end,gpointer user_data)871 on_delete_range (GtkTextBuffer *buffer,
872                  GtkTextIter   *start,
873                  GtkTextIter   *end,
874                  gpointer       user_data)
875 {
876 	GtkTextIter start_iter;
877 	GtkTextIter end_iter;
878 	gboolean keep_bookmark;
879 
880 	/* Nothing to do for us here. The bookmark, if any, will stay at the
881 	   beginning of the line due to its left gravity. */
882 	if (gtk_text_iter_get_line (start) == gtk_text_iter_get_line (end))
883 		return;
884 
885 	start_iter = *start;
886 	gtk_text_iter_set_line_offset (&start_iter, 0);
887 
888 	end_iter = *end;
889 	gtk_text_iter_set_line_offset (&end_iter, 0);
890 
891 	keep_bookmark = ((gtk_source_buffer_get_source_marks_at_iter (GTK_SOURCE_BUFFER (buffer),
892 								     &start_iter,
893 								     BOOKMARK_CATEGORY) != NULL) ||
894 			 (gtk_source_buffer_get_source_marks_at_iter (GTK_SOURCE_BUFFER (buffer),
895 								      &end_iter,
896 								      BOOKMARK_CATEGORY) != NULL));
897 
898 	/* Remove all bookmarks in the range. */
899 	gtk_source_buffer_remove_source_marks (GTK_SOURCE_BUFFER (buffer),
900 					       &start_iter,
901 					       &end_iter,
902 					       BOOKMARK_CATEGORY);
903 
904 	if (keep_bookmark)
905 		gtk_source_buffer_create_source_mark (GTK_SOURCE_BUFFER (buffer),
906 						      NULL,
907 						      BOOKMARK_CATEGORY,
908 						      &start_iter);
909 }
910 
911 static void
on_begin_user_action(GtkTextBuffer * buffer,InsertData * data)912 on_begin_user_action (GtkTextBuffer *buffer,
913 		      InsertData    *data)
914 {
915 	++data->user_action;
916 }
917 
918 static void
on_end_user_action(GtkTextBuffer * buffer,InsertData * data)919 on_end_user_action (GtkTextBuffer *buffer,
920 		    InsertData    *data)
921 {
922 	GSList *item;
923 
924 	if (--data->user_action > 0)
925 		return;
926 
927 	/* Remove trackers */
928 	for (item = data->trackers; item; item = g_slist_next (item))
929 	{
930 		InsertTracker *tracker = item->data;
931 		GtkTextIter curloc;
932 		GtkTextIter newloc;
933 
934 		/* Move the category to the line where the mark now is */
935 		gtk_text_buffer_get_iter_at_mark (buffer,
936 		                                  &curloc,
937 		                                  GTK_TEXT_MARK (tracker->bookmark));
938 
939 		gtk_text_buffer_get_iter_at_mark (buffer,
940 		                                  &newloc,
941 		                                  tracker->mark);
942 
943 		if (gtk_text_iter_get_line (&curloc) != gtk_text_iter_get_line (&newloc))
944 		{
945 			gtk_text_iter_set_line_offset (&newloc, 0);
946 			gtk_text_buffer_move_mark (buffer,
947 			                           GTK_TEXT_MARK (tracker->bookmark),
948 			                           &newloc);
949 		}
950 
951 		gtk_text_buffer_delete_mark (buffer, tracker->mark);
952 		g_slice_free (InsertTracker, tracker);
953 	}
954 
955 	g_slist_free (data->trackers);
956 	data->trackers = NULL;
957 }
958 
959 static void
add_tracker(GtkTextBuffer * buffer,GtkTextIter * iter,GtkSourceMark * bookmark,InsertData * data)960 add_tracker (GtkTextBuffer *buffer,
961              GtkTextIter   *iter,
962              GtkSourceMark *bookmark,
963              InsertData    *data)
964 {
965 	GSList *item;
966 	InsertTracker *tracker;
967 
968 	for (item = data->trackers; item; item = g_slist_next (item))
969 	{
970 		tracker = item->data;
971 
972 		if (tracker->bookmark == bookmark)
973 			return;
974 	}
975 
976 	tracker = g_slice_new (InsertTracker);
977 	tracker->bookmark = bookmark;
978 	tracker->mark = gtk_text_buffer_create_mark (buffer,
979 	                                             NULL,
980 	                                             iter,
981 	                                             FALSE);
982 
983 	data->trackers = g_slist_prepend (data->trackers, tracker);
984 }
985 
986 static void
on_insert_text_before(GtkTextBuffer * buffer,GtkTextIter * location,gchar * text,gint len,InsertData * data)987 on_insert_text_before (GtkTextBuffer *buffer,
988 		       GtkTextIter   *location,
989 		       gchar         *text,
990 		       gint	      len,
991 		       InsertData    *data)
992 {
993 	if (gtk_text_iter_starts_line (location))
994 	{
995 		GSList *marks;
996 		marks = gtk_source_buffer_get_source_marks_at_iter (GTK_SOURCE_BUFFER (buffer),
997 		                                                    location,
998 		                                                    BOOKMARK_CATEGORY);
999 
1000 		if (marks != NULL)
1001 		{
1002 			add_tracker (buffer, location, marks->data, data);
1003 			g_slist_free (marks);
1004 		}
1005 	}
1006 }
1007 
1008 static GtkSourceMark *
get_bookmark_and_iter(GtkSourceBuffer * buffer,GtkTextIter * iter,GtkTextIter * start)1009 get_bookmark_and_iter (GtkSourceBuffer *buffer,
1010                        GtkTextIter     *iter,
1011                        GtkTextIter     *start)
1012 {
1013 	GSList        *marks;
1014 	GtkSourceMark *ret = NULL;
1015 
1016 	if (!iter)
1017 		gtk_text_buffer_get_iter_at_mark (GTK_TEXT_BUFFER (buffer),
1018 		                                  start,
1019 		                                  gtk_text_buffer_get_insert (GTK_TEXT_BUFFER (buffer)));
1020 	else
1021 		*start = *iter;
1022 
1023 	gtk_text_iter_set_line_offset (start, 0);
1024 
1025 	marks = gtk_source_buffer_get_source_marks_at_iter (buffer, start, BOOKMARK_CATEGORY);
1026 	if (marks != NULL)
1027 		ret = GTK_SOURCE_MARK (marks->data);
1028 
1029 	g_slist_free (marks);
1030 	return ret;
1031 }
1032 
1033 static void
remove_bookmark(GtkSourceBuffer * buffer,GtkTextIter * iter)1034 remove_bookmark (GtkSourceBuffer *buffer,
1035                  GtkTextIter     *iter)
1036 {
1037 	GtkTextIter    start;
1038 	GtkSourceMark *bookmark;
1039 
1040 	if ((bookmark = get_bookmark_and_iter (buffer, iter, &start)) != NULL)
1041 		gtk_text_buffer_delete_mark (GTK_TEXT_BUFFER (buffer),
1042 		                             GTK_TEXT_MARK (bookmark));
1043 }
1044 
1045 static void
add_bookmark(GtkSourceBuffer * buffer,GtkTextIter * iter)1046 add_bookmark (GtkSourceBuffer *buffer,
1047               GtkTextIter     *iter)
1048 {
1049 	GtkTextIter    start;
1050 	GtkSourceMark *bookmark;
1051 
1052 	if ((bookmark = get_bookmark_and_iter (buffer, iter, &start)) == NULL)
1053 		gtk_source_buffer_create_source_mark (GTK_SOURCE_BUFFER (buffer),
1054 						      NULL,
1055 						      BOOKMARK_CATEGORY,
1056 						      &start);
1057 }
1058 
1059 static void
toggle_bookmark(GtkSourceBuffer * buffer,GtkTextIter * iter)1060 toggle_bookmark (GtkSourceBuffer *buffer,
1061                  GtkTextIter     *iter)
1062 {
1063 	GtkTextIter    start;
1064 	GtkSourceMark *bookmark = NULL;
1065 
1066 	g_return_if_fail (buffer != NULL);
1067 
1068 	if ((bookmark = get_bookmark_and_iter (buffer, iter, &start)) != NULL)
1069 		remove_bookmark (buffer, &start);
1070 	else
1071 		add_bookmark (buffer, &start);
1072 }
1073 
1074 static void
on_toggle_bookmark_activate(GtkAction * action,PlumaBookmarksPlugin * plugin)1075 on_toggle_bookmark_activate (GtkAction            *action,
1076                              PlumaBookmarksPlugin *plugin)
1077 {
1078 	toggle_bookmark (GTK_SOURCE_BUFFER (pluma_window_get_active_document (plugin->priv->window)),
1079 	                 NULL);
1080 }
1081 
1082 static void
on_next_bookmark_activate(GtkAction * action,PlumaBookmarksPlugin * plugin)1083 on_next_bookmark_activate (GtkAction            *action,
1084                           PlumaBookmarksPlugin *plugin)
1085 {
1086 	goto_bookmark (plugin->priv->window,
1087 	               NULL,
1088 	               NULL,
1089 	               gtk_source_buffer_forward_iter_to_source_mark,
1090 	               gtk_text_buffer_get_start_iter);
1091 }
1092 
1093 static void
on_previous_bookmark_activate(GtkAction * action,PlumaBookmarksPlugin * plugin)1094 on_previous_bookmark_activate (GtkAction            *action,
1095                                PlumaBookmarksPlugin *plugin)
1096 {
1097 	goto_bookmark (plugin->priv->window,
1098 	               NULL,
1099 	               NULL,
1100 	               gtk_source_buffer_backward_iter_to_source_mark,
1101 	               gtk_text_buffer_get_end_iter);
1102 }
1103 
1104 static void
on_document_loaded(PlumaDocument * doc,const GError * error,PlumaView * view)1105 on_document_loaded (PlumaDocument *doc,
1106                     const GError  *error,
1107                     PlumaView     *view)
1108 {
1109 	if (error == NULL)
1110 	{
1111 		/* Reverting can leave one bookmark at the start, remove it. */
1112 		remove_all_bookmarks (GTK_SOURCE_BUFFER (doc));
1113 
1114 		load_bookmark_metadata (view);
1115 	}
1116 }
1117 
1118 static void
on_document_saved(PlumaDocument * doc,const GError * error,PlumaView * view)1119 on_document_saved (PlumaDocument *doc,
1120                    const GError  *error,
1121                    PlumaView     *view)
1122 {
1123 	if (error == NULL)
1124 		save_bookmark_metadata (view);
1125 }
1126 
1127 static void
on_tab_added(PlumaWindow * window,PlumaTab * tab,PlumaBookmarksPlugin * plugin)1128 on_tab_added (PlumaWindow          *window,
1129               PlumaTab             *tab,
1130               PlumaBookmarksPlugin *plugin)
1131 {
1132 	PlumaDocument *doc;
1133 	PlumaView     *view;
1134 
1135 	doc  = pluma_tab_get_document (tab);
1136 	view = pluma_tab_get_view (tab);
1137 
1138 	g_signal_connect (doc, "loaded",
1139 			  G_CALLBACK (on_document_loaded),
1140 			  view);
1141 	g_signal_connect (doc, "saved",
1142 			  G_CALLBACK (on_document_saved),
1143 			  view);
1144 
1145 	enable_bookmarks (view, plugin);
1146 }
1147 
1148 static void
on_tab_removed(PlumaWindow * window,PlumaTab * tab,PlumaBookmarksPlugin * plugin)1149 on_tab_removed (PlumaWindow          *window,
1150                 PlumaTab             *tab,
1151                 PlumaBookmarksPlugin *plugin)
1152 {
1153 	PlumaDocument *doc;
1154 	PlumaView     *view;
1155 
1156 	doc  = pluma_tab_get_document (tab);
1157 	view = pluma_tab_get_view (tab);
1158 
1159 	g_signal_handlers_disconnect_by_func (doc, on_document_loaded, view);
1160 	g_signal_handlers_disconnect_by_func (doc, on_document_saved, view);
1161 
1162 	disable_bookmarks (view);
1163 }
1164 
1165 static void
pluma_window_activatable_iface_init(PlumaWindowActivatableInterface * iface)1166 pluma_window_activatable_iface_init (PlumaWindowActivatableInterface *iface)
1167 {
1168 	iface->activate     = pluma_bookmarks_plugin_activate;
1169 	iface->deactivate   = pluma_bookmarks_plugin_deactivate;
1170 	iface->update_state = pluma_bookmarks_plugin_update_state;
1171 }
1172 
1173 G_MODULE_EXPORT void
peas_register_types(PeasObjectModule * module)1174 peas_register_types (PeasObjectModule *module)
1175 {
1176 	pluma_bookmarks_plugin_register_type (G_TYPE_MODULE (module));
1177 
1178 	peas_object_module_register_extension_type (module,
1179 						    PLUMA_TYPE_WINDOW_ACTIVATABLE,
1180 						    PLUMA_TYPE_BOOKMARKS_PLUGIN);
1181 }
1182 
1183 /* ex:set ts=8 noet: */
1184