1 /*
2 *
3 * Copyright (C) 2014 Colomban Wendling <ban@herbesfolles.org>
4 *
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 *
18 */
19
20 #include "config.h"
21
22 #include <string.h>
23
24 #include <glib.h>
25 #include <glib/gi18n-lib.h>
26 #include <gio/gio.h>
27 #include <gtk/gtk.h>
28
29 #include <git2.h>
30
31 #include <geanyplugin.h>
32 #include <geany.h>
33 #include <document.h>
34
35 #if ! defined (LIBGIT2_VER_MINOR) || ( (LIBGIT2_VER_MAJOR == 0) && (LIBGIT2_VER_MINOR < 22) )
36 # define git_libgit2_init git_threads_init
37 # define git_libgit2_shutdown git_threads_shutdown
38 #endif
39 #if ! defined (LIBGIT2_VER_MINOR) || ( (LIBGIT2_VER_MAJOR == 0) && (LIBGIT2_VER_MINOR < 23) )
40 /* 0.23 added @p binary_cb */
41 # define git_diff_buffers(old_buffer, old_len, old_as_path, \
42 new_buffer, new_len, new_as_path, options, \
43 file_cb, binary_cb, hunk_cb, line_cb, payload) \
44 git_diff_buffers (old_buffer, old_len, old_as_path, \
45 new_buffer, new_len, new_as_path, options, \
46 file_cb, hunk_cb, line_cb, payload)
47 #endif
48 #if ! defined (LIBGIT2_VER_MINOR) || ( (LIBGIT2_VER_MAJOR == 0) && (LIBGIT2_VER_MINOR < 28) )
49 # define git_buf_dispose git_buf_free
50 # define git_error_last giterr_last
51 #endif
52
53
54 GeanyPlugin *geany_plugin;
55 GeanyData *geany_data;
56
57
58 PLUGIN_VERSION_CHECK(224)
59
60 PLUGIN_SET_TRANSLATABLE_INFO (
61 LOCALEDIR, GETTEXT_PACKAGE,
62 _("Git Change Bar"),
63 _("Highlights uncommitted changes in files tracked with Git"),
64 "0.1",
65 "Colomban Wendling <ban@herbesfolles.org>"
66 )
67
68
69 /* g_async_queue_push() doesn't allow for NULL data, so use a non-NULL fake
70 * data that we know cannot ever be a valid job */
71 #define QUIT_THREAD_JOB ((AsyncBlobContentsJob *) (&G_queue))
72
73 #define RESOURCES_ALLOCATED_QTAG \
74 (g_quark_from_string (PLUGIN"/git-resources-allocated"))
75 #define UNDO_LINE_QTAG \
76 (g_quark_from_string (PLUGIN"/git-undo-line"))
77 #define DOC_ID_QTAG \
78 (g_quark_from_string (PLUGIN"/git-doc-id"))
79
80 #define REMOVED_MARKER_POS(pos) \
81 ((pos) == 0 ? 0 : (pos) - 1)
82
83 enum {
84 MARKER_LINE_ADDED,
85 MARKER_LINE_CHANGED,
86 MARKER_LINE_REMOVED,
87 MARKER_COUNT
88 };
89
90 enum {
91 KB_GOTO_PREV_HUNK,
92 KB_GOTO_NEXT_HUNK,
93 KB_UNDO_HUNK,
94 KB_COUNT
95 };
96
97 typedef void (*BlobContentsReadyFunc) (const gchar *path,
98 git_buf *buf,
99 gpointer data);
100
101 typedef struct AsyncBlobContentsJob AsyncBlobContentsJob;
102 struct AsyncBlobContentsJob {
103 gboolean force;
104 guint tag;
105 gchar *path;
106 git_buf buf;
107 BlobContentsReadyFunc callback;
108 gpointer user_data;
109 };
110
111 typedef struct TooltipHunkData TooltipHunkData;
112 struct TooltipHunkData {
113 gint line;
114 gboolean found;
115 GeanyDocument *doc;
116 const git_buf *buf;
117 GtkTooltip *tooltip;
118 };
119
120 #define TOOLTIP_HUNK_DATA_INIT(line, doc, buf, tooltip) \
121 { line, FALSE, doc, buf, tooltip }
122
123 typedef struct GotoNextHunkData GotoNextHunkData;
124 struct GotoNextHunkData {
125 guint kb;
126 guint doc_id;
127 gint line;
128 gint next_line;
129 };
130
131 typedef struct UndoHunkData UndoHunkData;
132 struct UndoHunkData {
133 guint doc_id;
134 gint line;
135 gboolean found;
136 gint old_start;
137 gint old_lines;
138 gint new_start;
139 gint new_lines;
140 };
141
142 static void on_git_repo_changed (GFileMonitor *monitor,
143 GFile *file,
144 GFile *other_file,
145 GFileMonitorEvent event_type,
146 gpointer force);
147 static gboolean on_sci_query_tooltip (GtkWidget *widget,
148 gint x,
149 gint y,
150 gboolean keyboard_mode,
151 GtkTooltip *tooltip,
152 gpointer user_data);
153 static void read_setting_color (GKeyFile *kf,
154 const gchar *group,
155 const gchar *key,
156 gpointer value);
157 static void write_setting_color (GKeyFile *kf,
158 const gchar *group,
159 const gchar *key,
160 gconstpointer value);
161 static void read_setting_boolean (GKeyFile *kf,
162 const gchar *group,
163 const gchar *key,
164 gpointer value);
165 static void write_setting_boolean (GKeyFile *kf,
166 const gchar *group,
167 const gchar *key,
168 gconstpointer value);
169
170
171 /* cache */
172 static git_buf G_blob_contents = { 0 };
173 static guint G_blob_contents_tag = 0;
174 /* global state */
175 static GAsyncQueue *G_queue = NULL;
176 static GThread *G_thread = NULL;
177 static gulong G_source_id = 0;
178 static gboolean G_monitoring_enabled = TRUE;
179 static GtkWidget *G_undo_menu_item = NULL;
180 static struct {
181 gint num;
182 gint style;
183 guint32 color;
184 } G_markers[MARKER_COUNT] = {
185 { -1, SC_MARK_LEFTRECT, 0x73d216 },
186 { -1, SC_MARK_LEFTRECT, 0xf57900 },
187 { -1, SC_MARK_LEFTRECT, 0xcc0000 }
188 };
189 /* settings description */
190 static const struct {
191 const gchar *group;
192 const gchar *key;
193 gpointer value;
194 void (*read) (GKeyFile *kf,
195 const gchar *group,
196 const gchar *key,
197 gpointer value);
198 void (*write) (GKeyFile *kf,
199 const gchar *group,
200 const gchar *key,
201 gconstpointer value);
202 } G_settings_desc[] = {
203 { "general", "monitor-repository", &G_monitoring_enabled,
204 read_setting_boolean, write_setting_boolean },
205 { "colors", "line-added", &G_markers[MARKER_LINE_ADDED].color,
206 read_setting_color, write_setting_color },
207 { "colors", "line-changed", &G_markers[MARKER_LINE_CHANGED].color,
208 read_setting_color, write_setting_color },
209 { "colors", "line-removed", &G_markers[MARKER_LINE_REMOVED].color,
210 read_setting_color, write_setting_color }
211 };
212
213
214 /* workaround https://github.com/libgit2/libgit2/pull/3187 */
215 static int
gcb_git_buf_grow(git_buf * buf,size_t target_size)216 gcb_git_buf_grow (git_buf *buf,
217 size_t target_size)
218 {
219 if (buf->asize == 0) {
220 if (target_size == 0) {
221 target_size = buf->size;
222 }
223 if ((target_size & 7) == 0) {
224 target_size++;
225 }
226 }
227 return git_buf_grow (buf, target_size);
228 }
229 #define git_buf_grow gcb_git_buf_grow
230
231 static void
buf_zero(git_buf * buf)232 buf_zero (git_buf *buf)
233 {
234 if (buf) {
235 buf->ptr = NULL;
236 buf->size = 0;
237 buf->asize = 0;
238 }
239 }
240
241 static void
clear_cached_blob_contents(void)242 clear_cached_blob_contents (void)
243 {
244 if (G_blob_contents.ptr) {
245 git_buf_dispose (&G_blob_contents);
246 buf_zero (&G_blob_contents);
247 }
248 G_blob_contents_tag = 0;
249 }
250
251 /* get the file blob for @relpath at HEAD */
252 static gboolean
repo_get_file_blob_contents(git_repository * repo,const gchar * relpath,git_buf * contents,int check_for_binary_data)253 repo_get_file_blob_contents (git_repository *repo,
254 const gchar *relpath,
255 git_buf *contents,
256 int check_for_binary_data)
257 {
258 git_reference *head = NULL;
259 gboolean success = FALSE;
260
261 if (git_repository_head (&head, repo) == 0) {
262 git_commit *commit = NULL;
263
264 if (git_commit_lookup (&commit, repo, git_reference_target (head)) == 0) {
265 git_tree *tree = NULL;
266
267 if (git_commit_tree (&tree, commit) == 0) {
268 git_tree_entry *entry = NULL;
269
270 if (git_tree_entry_bypath (&entry, tree, relpath) == 0) {
271 git_blob *blob;
272
273 if (git_blob_lookup (&blob, repo, git_tree_entry_id (entry)) == 0) {
274 if (git_blob_filtered_content (contents, blob, relpath,
275 check_for_binary_data) == 0 &&
276 git_buf_grow (contents, 0) == 0) {
277 success = TRUE;
278 }
279 git_blob_free (blob);
280 }
281 git_tree_entry_free (entry);
282 }
283 git_tree_free (tree);
284 }
285 git_commit_free (commit);
286 }
287 git_reference_free (head);
288 }
289
290 return success;
291 }
292
293 static void
free_job(gpointer data)294 free_job (gpointer data)
295 {
296 AsyncBlobContentsJob *job = data;
297
298 /* unlikely, but if we still have the buffer, free it */
299 if (job->buf.ptr) {
300 git_buf_dispose (&job->buf);
301 }
302 g_free (job->path);
303 g_slice_free1 (sizeof *job, job);
304 }
305
306 static gboolean
report_work_in_idle(gpointer data)307 report_work_in_idle (gpointer data)
308 {
309 AsyncBlobContentsJob *job = data;
310
311 /* update cached blob */
312 clear_cached_blob_contents ();
313 G_blob_contents = job->buf;
314 G_blob_contents_tag = job->buf.ptr ? job->tag : 0;
315
316 job->callback (job->path, job->buf.ptr ? &job->buf : NULL, job->user_data);
317
318 buf_zero (&job->buf);
319
320 return FALSE;
321 }
322
323 static GFileMonitor *
monitor_repo_file(git_repository * repo,const gchar * subpath,GCallback changed_callback,gpointer user_data)324 monitor_repo_file (git_repository *repo,
325 const gchar *subpath,
326 GCallback changed_callback,
327 gpointer user_data)
328 {
329 GFile *file = NULL;
330 GError *err = NULL;
331 GFileMonitor *monitor = NULL;
332 gchar *path = g_build_filename (git_repository_path (repo),
333 subpath, NULL);
334
335 file = g_file_new_for_path (path);
336 monitor = g_file_monitor (file, 0, NULL, &err);
337 if (err) {
338 g_warning ("Failed to monitor %s: %s", path, err->message);
339 g_error_free (err);
340 } else {
341 g_signal_connect (monitor, "changed", changed_callback, user_data);
342 }
343 g_object_unref (file);
344 g_free (path);
345
346 return monitor;
347 }
348
349 /* monitors the current reference HEAD points to (probably a branch) */
350 static GFileMonitor *
monitor_head_ref(git_repository * repo,GCallback changed_callback,gpointer user_data)351 monitor_head_ref (git_repository *repo,
352 GCallback changed_callback,
353 gpointer user_data)
354 {
355 git_reference *head = NULL;
356 GFileMonitor *monitor = NULL;
357
358 if (! git_repository_head_detached (repo) &&
359 git_repository_head (&head, repo) == 0) {
360 monitor = monitor_repo_file (repo, git_reference_name (head),
361 changed_callback, user_data);
362 git_reference_free (head);
363 }
364
365 return monitor;
366 }
367
368 /* checks whether @path points somewhere inside @dir and returns the pointer
369 * inside @path starting the relative path, or NULL */
370 static const gchar *
path_dir_contains(const gchar * dir,const gchar * path)371 path_dir_contains (const gchar *dir,
372 const gchar *path)
373 {
374 #ifdef G_OS_WIN32
375 /* FIXME: handle drive letters and such */
376 # define NORM_PATH_CH(c) (((c) == '\\') ? '/' : (c))
377 #else
378 # define NORM_PATH_CH(c) (c)
379 #endif
380
381 g_return_val_if_fail (dir != NULL, NULL);
382 g_return_val_if_fail (path != NULL, NULL);
383
384 while (*dir && NORM_PATH_CH (*dir) == NORM_PATH_CH (*path)) {
385 dir++, path++;
386 }
387
388 return *dir ? NULL : path;
389 }
390
391 /* gets the Git path for @repo pointing to @sys_path, or NULL */
392 static gchar *
get_path_in_repository(git_repository * repo,const gchar * sys_path)393 get_path_in_repository (git_repository *repo,
394 const gchar *sys_path)
395 {
396 const gchar *workdir = git_repository_workdir (repo);
397 const gchar *rel_path = path_dir_contains (workdir, sys_path);
398
399 #ifdef G_OS_WIN32
400 if (rel_path) {
401 /* we want an internal Git path, which uses UNIX format */
402 gchar *p;
403 gchar *repo_path = g_strdup (rel_path);
404
405 for (p = repo_path; *p; p++) {
406 if (*p == '\\') {
407 *p = '/';
408 }
409 }
410
411 return repo_path;
412 }
413
414 return NULL;
415 #else
416 return g_strdup (rel_path);
417 #endif
418 }
419
420 static gpointer
worker_thread(gpointer data)421 worker_thread (gpointer data)
422 {
423 GAsyncQueue *queue = data;
424 git_repository *repo = NULL;
425 GFileMonitor *monitors[2] = { NULL, NULL };
426 AsyncBlobContentsJob *job;
427 guint i;
428
429 while ((job = g_async_queue_pop (queue)) != QUIT_THREAD_JOB) {
430 const gchar *path = job->path;
431
432 if (repo && (job->force ||
433 ! path_dir_contains (path, git_repository_workdir (repo)))) {
434 /* FIXME: this can fail with nested repositories */
435 git_repository_free (repo);
436 repo = NULL;
437 for (i = 0; i < G_N_ELEMENTS (monitors); i++) {
438 if (monitors[i]) {
439 g_object_unref (monitors[i]);
440 monitors[i] = NULL;
441 }
442 }
443 }
444 if (! repo) {
445 gchar *dirname = g_path_get_dirname (path);
446 if (git_repository_open_ext (&repo, dirname, 0, NULL) == 0) {
447 if (git_repository_is_bare (repo)) {
448 git_repository_free (repo);
449 repo = NULL;
450 } else if (G_monitoring_enabled) {
451 /* we need to monitor HEAD, in case of e.g. branch switch (e.g.
452 * git checkout -b will switch the ref we need to watch) */
453 monitors[0] = monitor_repo_file (repo, "HEAD",
454 G_CALLBACK (on_git_repo_changed),
455 GINT_TO_POINTER (TRUE));
456 /* and of course the real ref (branch) for when changes get committed */
457 monitors[1] = monitor_head_ref (repo, G_CALLBACK (on_git_repo_changed),
458 GINT_TO_POINTER (FALSE));
459 }
460 }
461 g_free(dirname);
462 }
463
464 buf_zero (&job->buf);
465 if (repo) {
466 gchar *relpath = get_path_in_repository (repo, path);
467
468 if (relpath) {
469 if (! repo_get_file_blob_contents (repo, relpath, &job->buf, 0)) {
470 git_buf_dispose (&job->buf);
471 buf_zero (&job->buf);
472 }
473
474 g_free (relpath);
475 }
476 }
477
478 g_idle_add_full (G_PRIORITY_LOW, report_work_in_idle, job, free_job);
479 }
480
481 for (i = 0; i < G_N_ELEMENTS (monitors); i++) {
482 if (monitors[i]) {
483 g_object_unref (monitors[i]);
484 monitors[i] = NULL;
485 }
486 }
487 if (repo) {
488 git_repository_free (repo);
489 }
490
491 return NULL;
492 }
493
494 static void
get_cached_blob_contents_async(const gchar * path,guint tag,gboolean force,BlobContentsReadyFunc callback,gpointer user_data)495 get_cached_blob_contents_async (const gchar *path,
496 guint tag,
497 gboolean force,
498 BlobContentsReadyFunc callback,
499 gpointer user_data)
500 {
501 if ((! force && G_blob_contents.ptr && tag == G_blob_contents_tag) ||
502 ! path) {
503 callback (path, &G_blob_contents, user_data);
504 } else {
505 AsyncBlobContentsJob *job = g_slice_alloc (sizeof *job);
506
507 job->force = force;
508 job->tag = tag;
509 job->path = g_strdup (path);
510 job->callback = callback;
511 job->user_data = user_data;
512 buf_zero (&job->buf);
513
514 if (! G_thread) {
515 G_queue = g_async_queue_new ();
516 #if GLIB_CHECK_VERSION (2, 32, 0)
517 G_thread = g_thread_new (PLUGIN"/blob-worker", worker_thread, G_queue);
518 #else
519 G_thread = g_thread_create (worker_thread, G_queue, FALSE, NULL);
520 #endif
521 }
522
523 g_async_queue_push (G_queue, job);
524 }
525 }
526
527 static gint
allocate_marker(ScintillaObject * sci,guint marker)528 allocate_marker (ScintillaObject *sci,
529 guint marker)
530 {
531 g_return_val_if_fail (marker < MARKER_COUNT, -1);
532
533 if (G_markers[marker].num == -1) {
534 gint i;
535
536 G_markers[marker].num = -2;
537 /* markers 0-1 and 25-31 are reserved */
538 for (i = 2; G_markers[marker].num < 0 && i < 25; i++) {
539 gint sym = scintilla_send_message (sci, SCI_MARKERSYMBOLDEFINED, i, 0);
540
541 if (sym == SC_MARK_AVAILABLE ||
542 sym == 0 /* unfortunately it's also SC_MARK_CIRCLE */) {
543 guint j;
544
545 /* check if we already allocate it but not defined it yet */
546 for (j = 0; j < MARKER_COUNT && G_markers[j].num != i; j++) {
547 /* nothing */
548 }
549 if (j == MARKER_COUNT) {
550 G_markers[marker].num = i;
551 }
552 }
553 }
554 }
555
556 return G_markers[marker].num;
557 }
558
559 static gboolean
allocate_resources(ScintillaObject * sci)560 allocate_resources (ScintillaObject *sci)
561 {
562 guint i;
563
564 if (g_object_get_qdata (G_OBJECT (sci), RESOURCES_ALLOCATED_QTAG)) {
565 return TRUE;
566 }
567
568 /* allocate all markers first so we have all or nothing */
569 for (i = 0; i < MARKER_COUNT; i++) {
570 if (allocate_marker (sci, i) < 0) {
571 return FALSE;
572 }
573 }
574
575 for (i = 0; i < MARKER_COUNT; i++) {
576 scintilla_send_message (sci, SCI_MARKERDEFINE,
577 G_markers[i].num, G_markers[i].style);
578 scintilla_send_message (sci, SCI_MARKERSETBACK,
579 G_markers[i].num,
580 /* Scintilla uses BGR */
581 ((G_markers[i].color & 0xff0000) >> 16) |
582 ((G_markers[i].color & 0x00ff00)) |
583 ((G_markers[i].color & 0x0000ff) << 16));
584 }
585
586 /* setup tooltips */
587 gtk_widget_set_has_tooltip (GTK_WIDGET (sci), TRUE);
588 g_signal_connect (sci, "query-tooltip",
589 G_CALLBACK (on_sci_query_tooltip), NULL);
590
591 g_object_set_qdata (G_OBJECT (sci), RESOURCES_ALLOCATED_QTAG,
592 sci /* anything non-NULL */);
593
594 return TRUE;
595 }
596
597 static void
release_resources(ScintillaObject * sci)598 release_resources (ScintillaObject *sci)
599 {
600 if (g_object_get_qdata (G_OBJECT (sci), RESOURCES_ALLOCATED_QTAG)) {
601 guint j;
602
603 for (j = 0; j < MARKER_COUNT; j++) {
604 if (G_markers[j].num >= 0) {
605 scintilla_send_message (sci, SCI_MARKERDEFINE,
606 G_markers[j].num, SC_MARK_AVAILABLE);
607 }
608 }
609 g_signal_handlers_disconnect_by_func (sci, on_sci_query_tooltip, NULL);
610 g_object_set_qdata (G_OBJECT (sci), RESOURCES_ALLOCATED_QTAG, NULL);
611 }
612 }
613
614 /* checks whether @encoding needs to be converted to UTF-8 */
615 static gboolean
encoding_needs_conversion(const gchar * encoding)616 encoding_needs_conversion (const gchar *encoding)
617 {
618 return (encoding &&
619 ! utils_str_equal (encoding, "UTF-8") &&
620 ! utils_str_equal (encoding, "None"));
621 }
622
623 /*
624 * @brief Converts encoding
625 * @param buffer Input and output buffer
626 * @param length Input and output buffer length
627 * @param free_buffer whether @p buffer should be freed when replaced
628 * @param to Target encoding
629 * @param from Source encoding
630 * @param error Return location for errors, or @c NULL
631 * @returns @c TRUE if @p buffer should be freed, or @c FALSE otherwise.
632 *
633 * @warning This function has a very weird API, but it is practical for
634 * how it's used.
635 * @note the only way to tell if the conversion succeeded when @p free_buffer
636 * is @c TRUE is to compare the output value of @buffer against the
637 * one it had as an input value.
638 *
639 * Converts between encodings (using g_convert()) in-place.
640 *
641 * The @p buffer is both an input and output parameter. As an input
642 * parameter, it is used as a constant buffer pointer and is never
643 * modified nor freed. As an output parameter, it is an allocated chunk
644 * of memory that should be freed with @c g_free().
645 * If the conversion succeeds, it replaces @p buffer and @p length with
646 * the converted values and returns @c TRUE. If it fails, it does not
647 * modify the values and returns @p free_buffer so that the passed-in
648 * variables can be used as a fallback if the conversion was optional.
649 */
650 static gboolean
convert_encoding_inplace(gchar ** buffer,gsize * length,gboolean free_buffer,const gchar * to,const gchar * from,GError ** error)651 convert_encoding_inplace (gchar **buffer,
652 gsize *length,
653 gboolean free_buffer,
654 const gchar *to,
655 const gchar *from,
656 GError **error)
657 {
658 gsize tmp_len;
659 gchar *tmp_buf = g_convert (*buffer, (gssize) *length, to, from,
660 NULL, &tmp_len, error);
661
662 if (tmp_buf) {
663 if (free_buffer) {
664 g_free (*buffer);
665 }
666
667 *buffer = tmp_buf;
668 *length = tmp_len;
669 free_buffer = TRUE;
670 }
671
672 return free_buffer;
673 }
674
675 static gboolean
add_utf8_bom(gchar ** buffer,gsize * length,gboolean free_buffer)676 add_utf8_bom (gchar **buffer,
677 gsize *length,
678 gboolean free_buffer)
679 {
680 gchar *new_buf = g_malloc (*length + 3);
681
682 new_buf[0] = (gchar) 0xef;
683 new_buf[1] = (gchar) 0xbb;
684 new_buf[2] = (gchar) 0xbf;
685 memcpy (&new_buf[3], *buffer, *length);
686
687 if (free_buffer) {
688 g_free (*buffer);
689 }
690
691 *buffer = new_buf;
692 *length += 3;
693
694 return TRUE;
695 }
696
697 static int
diff_buf_to_doc(const git_buf * old_buf,GeanyDocument * doc,git_diff_hunk_cb hunk_cb,void * payload)698 diff_buf_to_doc (const git_buf *old_buf,
699 GeanyDocument *doc,
700 git_diff_hunk_cb hunk_cb,
701 void *payload)
702 {
703 ScintillaObject *sci = doc->editor->sci;
704 git_diff_options opts = GIT_DIFF_OPTIONS_INIT;
705 gchar *buf;
706 size_t len;
707 gboolean free_buf = FALSE;
708 int ret;
709
710 buf = (gchar *) scintilla_send_message (sci, SCI_GETCHARACTERPOINTER, 0, 0);
711 len = sci_get_length (sci);
712
713 /* add the BOM if needed */
714 if (doc->has_bom) {
715 /* UTF-8 BOM, converted below */
716 free_buf = add_utf8_bom (&buf, &len, free_buf);
717 }
718 /* convert the buffer back to in-file encoding if necessary */
719 if (encoding_needs_conversion (doc->encoding)) {
720 free_buf = convert_encoding_inplace (&buf, &len, free_buf,
721 doc->encoding, "UTF-8", NULL);
722 }
723
724 /* no context lines, and no need to bother about binary checks */
725 opts.context_lines = 0;
726 opts.flags = GIT_DIFF_FORCE_TEXT;
727
728 ret = git_diff_buffers (old_buf->ptr, old_buf->size, NULL,
729 buf, len, NULL, &opts, NULL, NULL, hunk_cb, NULL,
730 payload);
731
732 if (free_buf) {
733 g_free (buf);
734 }
735
736 return ret;
737 }
738
739 static int
diff_hunk_cb(const git_diff_delta * delta,const git_diff_hunk * hunk,void * data)740 diff_hunk_cb (const git_diff_delta *delta,
741 const git_diff_hunk *hunk,
742 void *data)
743 {
744 ScintillaObject *sci = data;
745 gint line;
746
747 if (hunk->new_lines > 0) {
748 guint marker = hunk->old_lines > 0 ? MARKER_LINE_CHANGED : MARKER_LINE_ADDED;
749
750 for (line = hunk->new_start; line < hunk->new_start + hunk->new_lines; line++) {
751 scintilla_send_message (sci, SCI_MARKERADD, line - 1, G_markers[marker].num);
752 }
753 } else {
754 line = REMOVED_MARKER_POS (hunk->new_start);
755 scintilla_send_message (sci, SCI_MARKERADD, line,
756 G_markers[MARKER_LINE_REMOVED].num);
757 }
758
759 return 0;
760 }
761
762 static GtkWidget *
get_widget_for_buf_range(GeanyDocument * doc,const git_buf * contents,gint line_start,gint n_lines)763 get_widget_for_buf_range (GeanyDocument *doc,
764 const git_buf *contents,
765 gint line_start,
766 gint n_lines)
767 {
768 ScintillaObject *sci = editor_create_widget (doc->editor);
769 const GeanyIndentPrefs *iprefs = editor_get_indent_prefs (doc->editor);
770 gint width = 0;
771 gint height = 0;
772 gint zoom;
773 gint i;
774 GtkAllocation alloc;
775 gchar *buf = contents->ptr;
776 gsize buf_len = contents->size;
777 gboolean free_buf = FALSE;
778
779 gtk_widget_get_allocation (GTK_WIDGET (doc->editor->sci), &alloc);
780
781 highlighting_set_styles (sci, doc->file_type);
782 if (iprefs->type == GEANY_INDENT_TYPE_BOTH) {
783 scintilla_send_message (sci, SCI_SETTABWIDTH, iprefs->hard_tab_width, 0);
784 } else {
785 scintilla_send_message (sci, SCI_SETTABWIDTH, iprefs->width, 0);
786 }
787 scintilla_send_message (sci, SCI_SETINDENT, iprefs->width, 0);
788 zoom = scintilla_send_message (doc->editor->sci, SCI_GETZOOM, 0, 0);
789 scintilla_send_message (sci, SCI_SETZOOM, zoom, 0);
790
791 /* hide stuff we don't wanna see */
792 scintilla_send_message (sci, SCI_SETHSCROLLBAR, 0, 0);
793 scintilla_send_message (sci, SCI_SETVSCROLLBAR, 0, 0);
794 for (i = 0; i < SC_MAX_MARGIN; i++) {
795 scintilla_send_message (sci, SCI_SETMARGINWIDTHN, i, 0);
796 }
797
798 /* convert the buffer to UTF-8 if necessary */
799 if (encoding_needs_conversion (doc->encoding)) {
800 free_buf = convert_encoding_inplace (&buf, &buf_len, free_buf,
801 "UTF-8", doc->encoding, NULL);
802 }
803
804 scintilla_send_message (sci, SCI_ADDTEXT, buf_len, (sptr_t) buf);
805
806 if (free_buf) {
807 g_free (buf);
808 }
809
810 /* we need to enable extra scroll after last line so that SETFIRSTVISIBLELINE
811 * really places the line we want on top of the view, even if the line is
812 * close to the end and wouldn't possibly end on top otherwise */
813 scintilla_send_message (sci, SCI_SETENDATLASTLINE, 0, 0);
814 scintilla_send_message (sci, SCI_SETFIRSTVISIBLELINE, line_start, 0);
815
816 /* compute the size of the area we want to see */
817 for (i = line_start; i < line_start + n_lines; i++) {
818 gint pos = sci_get_line_end_position (sci, i);
819 gint end_x = scintilla_send_message (sci, SCI_POINTXFROMPOSITION, 0, pos);
820
821 height += scintilla_send_message (sci, SCI_TEXTHEIGHT, i, 0);
822 width = MAX (width, end_x);
823
824 if (height > alloc.height) {
825 break;
826 }
827 }
828 /* We need 2 extra pixels of width:
829 * 1 to avoid cropping the rightmost vertical bar of letters like H and M,
830 * 1 to avoid spurious line wrapping (issue #425). */
831 gtk_widget_set_size_request (GTK_WIDGET (sci),
832 MIN (width + 2, alloc.width),
833 MIN (height + 1, alloc.height));
834
835 return GTK_WIDGET (sci);
836 }
837
838 static gboolean
is_first_line_removed(gint line,gint new_hunk_start,gint new_hunk_lines)839 is_first_line_removed (gint line, gint new_hunk_start, gint new_hunk_lines)
840 {
841 return line == 1 && new_hunk_start == 0 && new_hunk_lines == 0;
842 }
843
844 static int
tooltip_diff_hunk_cb(const git_diff_delta * delta,const git_diff_hunk * hunk,void * data)845 tooltip_diff_hunk_cb (const git_diff_delta *delta,
846 const git_diff_hunk *hunk,
847 void *data)
848 {
849 TooltipHunkData *thd = data;
850
851 if (thd->found) {
852 return 1;
853 }
854
855 if (hunk->old_lines > 0 &&
856 (is_first_line_removed (thd->line, hunk->new_start, hunk->new_lines) ||
857 (thd->line >= hunk->new_start &&
858 thd->line < hunk->new_start + MAX (1, hunk->new_lines)))) {
859 GtkWidget *old = get_widget_for_buf_range (thd->doc, thd->buf,
860 hunk->old_start - 1,
861 hunk->old_lines);
862
863 gtk_tooltip_set_custom (thd->tooltip, old);
864 thd->found = old != NULL;
865 }
866
867 return thd->found;
868 }
869
870 static gboolean
on_sci_query_tooltip(GtkWidget * widget,gint x,gint y,gboolean keyboard_mode,GtkTooltip * tooltip,gpointer user_data)871 on_sci_query_tooltip (GtkWidget *widget,
872 gint x,
873 gint y,
874 gboolean keyboard_mode,
875 GtkTooltip *tooltip,
876 gpointer user_data)
877 {
878 gint min_x;
879 gint max_x;
880 ScintillaObject *sci = (ScintillaObject *) widget;
881 GeanyDocument *doc = document_get_current ();
882 gboolean has_tooltip = FALSE;
883
884 /* for some reason the widget isn't the current one during tab switch, so
885 * give up silently when we receive a query for a non-current widget */
886 if (! doc || doc->editor->sci != sci) {
887 return FALSE;
888 }
889
890 min_x = scintilla_send_message (sci, SCI_GETMARGINWIDTHN, 0, 0);
891 max_x = min_x + scintilla_send_message (sci, SCI_GETMARGINWIDTHN, 1, 0);
892
893 if (x >= min_x && x <= max_x &&
894 G_blob_contents.ptr && G_blob_contents_tag == doc->id) {
895 gint pos = scintilla_send_message (sci, SCI_POSITIONFROMPOINT, x, y);
896 gint line = sci_get_line_from_position (sci, pos);
897 gint mask = scintilla_send_message (sci, SCI_MARKERGET, line, 0);
898
899 if (mask & ((1 << G_markers[MARKER_LINE_CHANGED].num) |
900 (1 << G_markers[MARKER_LINE_REMOVED].num))) {
901 TooltipHunkData thd = TOOLTIP_HUNK_DATA_INIT (line + 1, doc,
902 &G_blob_contents, tooltip);
903
904 diff_buf_to_doc (&G_blob_contents, doc, tooltip_diff_hunk_cb, &thd);
905 has_tooltip = thd.found;
906 }
907 }
908
909 return has_tooltip;
910 }
911
912 static void
update_diff(const gchar * path,git_buf * contents,gpointer data)913 update_diff (const gchar *path,
914 git_buf *contents,
915 gpointer data)
916 {
917 GeanyDocument *doc = document_get_current ();
918
919 if (doc && doc->id == GPOINTER_TO_UINT (data)) {
920 ScintillaObject *sci = doc->editor->sci;
921 gboolean allocated = !! g_object_get_qdata (G_OBJECT (sci),
922 RESOURCES_ALLOCATED_QTAG);
923
924 if (allocated) {
925 guint i;
926
927 /* clear previous markers */
928 for (i = 0; i < MARKER_COUNT; i++) {
929 scintilla_send_message (sci, SCI_MARKERDELETEALL, G_markers[i].num, 0);
930 }
931 }
932
933 gtk_widget_set_visible (G_undo_menu_item, contents != NULL);
934
935 if (contents && (allocated || allocate_resources (sci))) {
936 diff_buf_to_doc (contents, doc, diff_hunk_cb, sci);
937 } else if (! contents && allocated) {
938 /* if we don't have contents, it probably means the document doesn't
939 * match any object known by Git, so next attempts will fail just the
940 * same. So, drop allocated resources if any (if it used to be a valid
941 * object, e.g. the document was renamed to something unknown to Git) */
942 release_resources (sci);
943 }
944 }
945 }
946
947 static gboolean
do_update_diff_idle(guint doc_id,gboolean force)948 do_update_diff_idle (guint doc_id,
949 gboolean force)
950 {
951 GeanyDocument *doc = document_get_current ();
952
953 G_source_id = 0;
954 /* make sure the document is still valid and current */
955 if (doc && doc->id == doc_id) {
956 get_cached_blob_contents_async (doc->real_path, doc_id, force, update_diff,
957 GUINT_TO_POINTER (doc->id));
958 }
959
960 return FALSE;
961 }
962
963 static gboolean
update_diff_idle(gpointer id)964 update_diff_idle (gpointer id)
965 {
966 return do_update_diff_idle (GPOINTER_TO_UINT (id), FALSE);
967 }
968
969 static gboolean
update_diff_force_idle(gpointer id)970 update_diff_force_idle (gpointer id)
971 {
972 return do_update_diff_idle (GPOINTER_TO_UINT (id), TRUE);
973 }
974
975 /*
976 * @brief Pushes a request for updating the diff
977 * @param doc The document for which update the diff
978 * @param force Whether to force reloading the repository information. This
979 * is used to e.g. force re-setting up monitors after a repository
980 * change.
981 *
982 * Pushes a request for updating the diff. Typically this should be called
983 * after the user modified the buffer to keep the diff in sync.
984 *
985 * Pass @c TRUE to @p force if the repository might have changed in a way that
986 * requires reloading it. Note that generally you don't need to do so when the
987 * file might have changed in the repository (e.g. when the user checked out
988 * another ref) because then the diff is either still valid or the buffer needs
989 * reloading from disk anyway.
990 */
991 static void
update_diff_push(GeanyDocument * doc,gboolean force)992 update_diff_push (GeanyDocument *doc,
993 gboolean force)
994 {
995 g_return_if_fail (DOC_VALID (doc));
996
997 gtk_widget_hide (G_undo_menu_item);
998
999 if (G_source_id) {
1000 g_source_remove (G_source_id);
1001 G_source_id = 0;
1002 }
1003 if (doc->real_path) {
1004 G_source_id = g_timeout_add_full (G_PRIORITY_LOW, 100,
1005 force ? update_diff_force_idle
1006 : update_diff_idle,
1007 GUINT_TO_POINTER (doc->id), NULL);
1008 }
1009 }
1010
1011 static gboolean
on_editor_notify(GObject * obj,GeanyEditor * editor,SCNotification * nt,gpointer user_data)1012 on_editor_notify (GObject *obj,
1013 GeanyEditor *editor,
1014 SCNotification *nt,
1015 gpointer user_data)
1016 {
1017 if (nt->nmhdr.code == SCN_CHARADDED ||
1018 (nt->nmhdr.code == SCN_MODIFIED &&
1019 nt->modificationType & (SC_MOD_INSERTTEXT | SC_MOD_DELETETEXT))) {
1020 update_diff_push (editor->document, FALSE);
1021 }
1022
1023 return FALSE;
1024 }
1025
1026 static void
on_document_activate(GObject * obj,GeanyDocument * doc,gpointer user_data)1027 on_document_activate (GObject *obj,
1028 GeanyDocument *doc,
1029 gpointer user_data)
1030 {
1031 clear_cached_blob_contents ();
1032 update_diff_push (doc, FALSE);
1033 }
1034
1035 static void
on_startup_complete(GObject * obj,gpointer user_data)1036 on_startup_complete (GObject *obj,
1037 gpointer user_data)
1038 {
1039 GeanyDocument *doc = document_get_current ();
1040
1041 if (doc) {
1042 update_diff_push (doc, FALSE);
1043 }
1044 }
1045
1046 static void
on_git_repo_changed(GFileMonitor * monitor,GFile * file,GFile * other_file,GFileMonitorEvent event_type,gpointer force)1047 on_git_repo_changed (GFileMonitor *monitor,
1048 GFile *file,
1049 GFile *other_file,
1050 GFileMonitorEvent event_type,
1051 gpointer force)
1052 {
1053 GeanyDocument *doc = document_get_current ();
1054
1055 if (doc) {
1056 clear_cached_blob_contents ();
1057 update_diff_push (doc, GPOINTER_TO_INT (force));
1058 }
1059 }
1060
1061 static int
goto_next_hunk_diff_hunk_cb(const git_diff_delta * delta,const git_diff_hunk * hunk,void * udata)1062 goto_next_hunk_diff_hunk_cb (const git_diff_delta *delta,
1063 const git_diff_hunk *hunk,
1064 void *udata)
1065 {
1066 GotoNextHunkData *data = udata;
1067
1068 switch (data->kb) {
1069 case KB_GOTO_NEXT_HUNK:
1070 if (data->next_line >= 0) {
1071 return 1;
1072 } else if (data->line < hunk->new_start - 1) {
1073 data->next_line = REMOVED_MARKER_POS (hunk->new_start);
1074 }
1075 break;
1076
1077 case KB_GOTO_PREV_HUNK:
1078 if (data->line > hunk->new_start - 1 + MAX (hunk->new_lines - 1, 0)) {
1079 data->next_line = REMOVED_MARKER_POS (hunk->new_start);
1080 }
1081 break;
1082 }
1083
1084 return 0;
1085 }
1086
1087 static void
goto_next_hunk_cb(const gchar * path,git_buf * contents,gpointer udata)1088 goto_next_hunk_cb (const gchar *path,
1089 git_buf *contents,
1090 gpointer udata)
1091 {
1092 GotoNextHunkData *data = udata;
1093 GeanyDocument *doc = document_get_current ();
1094
1095 if (doc && doc->id == data->doc_id && contents) {
1096 diff_buf_to_doc (contents, doc, goto_next_hunk_diff_hunk_cb, data);
1097
1098 if (data->next_line >= 0) {
1099 gint pos = sci_get_position_from_line (doc->editor->sci, data->next_line);
1100
1101 editor_goto_pos (doc->editor, pos, FALSE);
1102 }
1103 }
1104
1105 g_slice_free1 (sizeof *data, data);
1106 }
1107
1108 static void
on_kb_goto_next_hunk(guint kb)1109 on_kb_goto_next_hunk (guint kb)
1110 {
1111 GeanyDocument *doc = document_get_current ();
1112
1113 if (doc) {
1114 GotoNextHunkData *data = g_slice_alloc (sizeof *data);
1115
1116 data->kb = kb;
1117 data->doc_id = doc->id;
1118 data->line = sci_get_current_line (doc->editor->sci);
1119 data->next_line = -1;
1120
1121 get_cached_blob_contents_async (doc->real_path, doc->id, FALSE,
1122 goto_next_hunk_cb, data);
1123 }
1124 }
1125
1126 static void
insert_buf_range(GeanyDocument * doc,const git_buf * old_contents,gint pos,gint old_start,gint old_lines)1127 insert_buf_range (GeanyDocument *doc,
1128 const git_buf *old_contents,
1129 gint pos,
1130 gint old_start,
1131 gint old_lines)
1132 {
1133 ScintillaObject *old_sci = editor_create_widget (doc->editor);
1134 gchar *old_buf = old_contents->ptr;
1135 gsize old_buf_len = old_contents->size;
1136 gboolean free_buf = FALSE;
1137 gint old_pos_start;
1138 gint old_pos_end;
1139 gchar *old_range;
1140
1141 /* convert the buffer to UTF-8 if necessary */
1142 if (encoding_needs_conversion (doc->encoding)) {
1143 free_buf = convert_encoding_inplace (&old_buf, &old_buf_len, free_buf,
1144 "UTF-8", doc->encoding, NULL);
1145 }
1146
1147 scintilla_send_message (old_sci, SCI_ADDTEXT, old_buf_len, (sptr_t) old_buf);
1148
1149 old_pos_start = sci_get_position_from_line (old_sci, old_start);
1150 old_pos_end = sci_get_position_from_line (old_sci, old_start + old_lines);
1151 old_range = sci_get_contents_range (old_sci, old_pos_start, old_pos_end);
1152
1153 sci_insert_text (doc->editor->sci, pos, old_range);
1154
1155 g_free (old_range);
1156
1157 if (free_buf) {
1158 g_free (old_buf);
1159 }
1160
1161 g_object_ref_sink (old_sci);
1162 g_object_unref (old_sci);
1163 }
1164
1165 static int
undo_hunk_diff_hunk_cb(const git_diff_delta * delta,const git_diff_hunk * hunk,void * udata)1166 undo_hunk_diff_hunk_cb (const git_diff_delta *delta,
1167 const git_diff_hunk *hunk,
1168 void *udata)
1169 {
1170 UndoHunkData *data = udata;
1171
1172 if (is_first_line_removed (data->line, hunk->new_start, hunk->new_lines) ||
1173 (data->line >= hunk->new_start &&
1174 data->line < hunk->new_start + MAX (1, hunk->new_lines))) {
1175 data->old_start = hunk->old_start;
1176 data->old_lines = hunk->old_lines;
1177 data->new_start = hunk->new_start;
1178 data->new_lines = hunk->new_lines;
1179 data->found = TRUE;
1180 return 1;
1181 }
1182
1183 return 0;
1184 }
1185
1186 static void
undo_hunk_cb(const gchar * path,git_buf * contents,gpointer udata)1187 undo_hunk_cb (const gchar *path,
1188 git_buf *contents,
1189 gpointer udata)
1190 {
1191 UndoHunkData *data = udata;
1192 GeanyDocument *doc = document_get_current ();
1193
1194 if (doc && doc->id == data->doc_id && contents) {
1195 diff_buf_to_doc (contents, doc, undo_hunk_diff_hunk_cb, data);
1196
1197 if (data->found) {
1198 ScintillaObject *sci = doc->editor->sci;
1199 gint line = data->new_start - (data->new_lines ? 1 : 0);
1200 gint pos = sci_get_position_from_line (sci, line);
1201
1202 sci_start_undo_action (sci);
1203
1204 if (data->new_lines > 0) {
1205 sci_set_target_start (sci, pos);
1206 pos = sci_get_position_from_line (sci, line + data->new_lines);
1207 sci_set_target_end (sci, pos);
1208 sci_replace_target (sci, "", FALSE);
1209 }
1210
1211 if (data->old_lines > 0) {
1212 pos = sci_get_position_from_line (sci, line);
1213 insert_buf_range (doc, contents, pos,
1214 data->old_start - 1,
1215 data->old_lines);
1216
1217 pos = sci_get_position_from_line (sci, line + data->old_lines);
1218 sci_set_current_position (sci, pos, FALSE);
1219 }
1220
1221 scintilla_send_message (sci, SCI_SCROLLRANGE,
1222 sci_get_position_from_line (sci, line),
1223 pos);
1224
1225 sci_end_undo_action (sci);
1226 }
1227 }
1228
1229 g_slice_free1 (sizeof *data, data);
1230 }
1231
1232 static void
undo_hunk(GeanyDocument * doc,gint line)1233 undo_hunk (GeanyDocument *doc,
1234 gint line)
1235 {
1236 UndoHunkData *data = g_slice_alloc (sizeof *data);
1237
1238 data->doc_id = doc->id;
1239 data->line = line + 1;
1240 data->found = FALSE;
1241
1242 get_cached_blob_contents_async (doc->real_path, doc->id, FALSE,
1243 undo_hunk_cb, data);
1244 }
1245
1246 static void
on_kb_undo_hunk(guint kb)1247 on_kb_undo_hunk (guint kb)
1248 {
1249 GeanyDocument *doc = document_get_current ();
1250
1251 if (doc) {
1252 undo_hunk (doc, sci_get_current_line (doc->editor->sci));
1253 }
1254 }
1255
1256 static void
on_undo_hunk_activate(GtkWidget * widget,gpointer user_data)1257 on_undo_hunk_activate (GtkWidget *widget,
1258 gpointer user_data)
1259 {
1260 GeanyDocument *doc = document_get_current ();
1261 gpointer doc_id = g_object_get_qdata (G_OBJECT (widget), DOC_ID_QTAG);
1262
1263 if (doc && doc->id == GPOINTER_TO_UINT (doc_id) &&
1264 gtk_widget_get_sensitive (widget)) {
1265 gpointer line = g_object_get_qdata (G_OBJECT (widget), UNDO_LINE_QTAG);
1266
1267 undo_hunk (doc, GPOINTER_TO_INT (line));
1268 }
1269 }
1270
1271 static void
check_undo_hunk_cb(const gchar * path,git_buf * contents,gpointer udata)1272 check_undo_hunk_cb (const gchar *path,
1273 git_buf *contents,
1274 gpointer udata)
1275 {
1276 UndoHunkData *data = udata;
1277 GeanyDocument *doc = document_get_current ();
1278
1279 if (doc && doc->id == data->doc_id && contents) {
1280 diff_buf_to_doc (contents, doc, undo_hunk_diff_hunk_cb, data);
1281 if (data->found) {
1282 gtk_widget_set_sensitive (G_undo_menu_item, TRUE);
1283 g_object_set_qdata (G_OBJECT (G_undo_menu_item), UNDO_LINE_QTAG,
1284 GINT_TO_POINTER (data->line - 1));
1285 g_object_set_qdata (G_OBJECT (G_undo_menu_item), DOC_ID_QTAG,
1286 GUINT_TO_POINTER (data->doc_id));
1287 }
1288 }
1289
1290 g_slice_free1 (sizeof *data, data);
1291 }
1292
1293 static void
on_update_editor_menu(GObject * object,const gchar * word,gint pos,GeanyDocument * doc,gpointer user_data)1294 on_update_editor_menu (GObject *object,
1295 const gchar *word,
1296 gint pos,
1297 GeanyDocument *doc,
1298 gpointer user_data)
1299 {
1300 gtk_widget_set_sensitive (G_undo_menu_item, FALSE);
1301
1302 if (doc) {
1303 UndoHunkData *data = g_slice_alloc (sizeof *data);
1304
1305 data->doc_id = doc->id;
1306 data->line = sci_get_line_from_position (doc->editor->sci, pos) + 1;
1307 data->found = FALSE;
1308
1309 get_cached_blob_contents_async (doc->real_path, doc->id, FALSE,
1310 check_undo_hunk_cb, data);
1311 }
1312 }
1313
1314 /* --- configuration loading and saving --- */
1315
1316 static void
read_setting_color(GKeyFile * kf,const gchar * group,const gchar * key,gpointer value)1317 read_setting_color (GKeyFile *kf,
1318 const gchar *group,
1319 const gchar *key,
1320 gpointer value)
1321 {
1322 guint32 *color = value;
1323 gchar *kfval = g_key_file_get_value (kf, group, key, NULL);
1324
1325 if (kfval) {
1326 const gchar *nptr = kfval;
1327 gchar *endptr;
1328 glong val;
1329
1330 if (*nptr == '#') {
1331 nptr++;
1332 }
1333
1334 val = strtol (nptr, &endptr, 16);
1335 if (! *endptr && val >= 0 && val <= 0xffffff) {
1336 *color = (guint32) val;
1337 }
1338 g_free (kfval);
1339 }
1340 }
1341
1342 static void
write_setting_color(GKeyFile * kf,const gchar * group,const gchar * key,gconstpointer value)1343 write_setting_color (GKeyFile *kf,
1344 const gchar *group,
1345 const gchar *key,
1346 gconstpointer value)
1347 {
1348 const guint32 *color = value;
1349 gchar kfval[8] = {0};
1350
1351 g_return_if_fail (*color <= 0xffffff);
1352
1353 g_snprintf (kfval, sizeof kfval, "#%.6x", *color);
1354 g_key_file_set_value (kf, group, key, kfval);
1355 }
1356
1357 static void
read_setting_boolean(GKeyFile * kf,const gchar * group,const gchar * key,gpointer value)1358 read_setting_boolean (GKeyFile *kf,
1359 const gchar *group,
1360 const gchar *key,
1361 gpointer value)
1362 {
1363 gboolean *bool = value;
1364
1365 *bool = utils_get_setting_boolean (kf, group, key, *bool);
1366 }
1367
1368 static void
write_setting_boolean(GKeyFile * kf,const gchar * group,const gchar * key,gconstpointer value)1369 write_setting_boolean (GKeyFile *kf,
1370 const gchar *group,
1371 const gchar *key,
1372 gconstpointer value)
1373 {
1374 const gboolean *bool = value;
1375
1376 g_key_file_set_boolean (kf, group, key, *bool);
1377 }
1378
1379 /* loads @filename in @kf and return %FALSE if failed, emitting a warning
1380 * unless the file was simply missing */
1381 static gboolean
read_keyfile(GKeyFile * kf,const gchar * filename,GKeyFileFlags flags)1382 read_keyfile (GKeyFile *kf,
1383 const gchar *filename,
1384 GKeyFileFlags flags)
1385 {
1386 GError *error = NULL;
1387
1388 if (! g_key_file_load_from_file (kf, filename, flags, &error)) {
1389 if (error->domain != G_FILE_ERROR || error->code != G_FILE_ERROR_NOENT) {
1390 g_warning (_("Failed to load configuration file: %s"), error->message);
1391 }
1392 g_error_free (error);
1393
1394 return FALSE;
1395 }
1396
1397 return TRUE;
1398 }
1399
1400 /* writes @kf in @filename, possibly creating directories to be able to write
1401 * in @filename */
1402 static gboolean
write_keyfile(GKeyFile * kf,const gchar * filename)1403 write_keyfile (GKeyFile *kf,
1404 const gchar *filename)
1405 {
1406 gchar *dirname = g_path_get_dirname (filename);
1407 GError *error = NULL;
1408 gint err;
1409 gchar *data;
1410 gsize length;
1411 gboolean success = FALSE;
1412
1413 data = g_key_file_to_data (kf, &length, NULL);
1414 if ((err = utils_mkdir (dirname, TRUE)) != 0) {
1415 g_warning (_("Failed to create configuration directory \"%s\": %s"),
1416 dirname, g_strerror (err));
1417 } else if (! g_file_set_contents (filename, data, (gssize) length, &error)) {
1418 g_warning (_("Failed to save configuration file: %s"), error->message);
1419 g_error_free (error);
1420 } else {
1421 success = TRUE;
1422 }
1423 g_free (data);
1424 g_free (dirname);
1425
1426 return success;
1427 }
1428
1429 static gchar *
get_config_filename(void)1430 get_config_filename (void)
1431 {
1432 return g_build_filename (geany_data->app->configdir, "plugins",
1433 PLUGIN, PLUGIN".conf", NULL);
1434 }
1435
1436 static void
load_config(void)1437 load_config (void)
1438 {
1439 gchar *filename = get_config_filename ();
1440 GKeyFile *kf = g_key_file_new ();
1441
1442 if (read_keyfile (kf, filename, G_KEY_FILE_NONE)) {
1443 guint i;
1444
1445 for (i = 0; i < G_N_ELEMENTS (G_settings_desc); i++) {
1446 G_settings_desc[i].read (kf, G_settings_desc[i].group,
1447 G_settings_desc[i].key,
1448 G_settings_desc[i].value);
1449 }
1450 }
1451 g_key_file_free (kf);
1452 g_free (filename);
1453 }
1454
1455 static void
save_config(void)1456 save_config (void)
1457 {
1458 gchar *filename = get_config_filename ();
1459 GKeyFile *kf = g_key_file_new ();
1460 guint i;
1461
1462 read_keyfile (kf, filename, G_KEY_FILE_KEEP_COMMENTS);
1463 for (i = 0; i < G_N_ELEMENTS (G_settings_desc); i++) {
1464 G_settings_desc[i].write (kf, G_settings_desc[i].group,
1465 G_settings_desc[i].key,
1466 G_settings_desc[i].value);
1467 }
1468 write_keyfile (kf, filename);
1469
1470 g_key_file_free (kf);
1471 g_free (filename);
1472 }
1473
1474 /* --- plugin initialization and cleanup --- */
1475
1476 void
plugin_init(GeanyData * data)1477 plugin_init (GeanyData *data)
1478 {
1479 GeanyKeyGroup *kb_group;
1480
1481 buf_zero (&G_blob_contents);
1482 G_blob_contents_tag = 0;
1483 G_source_id = 0;
1484 G_thread = NULL;
1485 G_queue = NULL;
1486
1487 if (git_libgit2_init () < 0) {
1488 const git_error *err = git_error_last ();
1489 g_warning ("Failed to initialize libgit2: %s", err ? err->message : "?");
1490 return;
1491 }
1492
1493 load_config ();
1494
1495 G_undo_menu_item = gtk_menu_item_new_with_label (_("Undo Git hunk"));
1496 g_signal_connect (G_undo_menu_item, "activate",
1497 G_CALLBACK (on_undo_hunk_activate), NULL);
1498 gtk_container_add (GTK_CONTAINER (data->main_widgets->editor_menu),
1499 G_undo_menu_item);
1500
1501 kb_group = plugin_set_key_group (geany_plugin, PLUGIN, KB_COUNT, NULL);
1502 keybindings_set_item (kb_group, KB_GOTO_PREV_HUNK, on_kb_goto_next_hunk, 0, 0,
1503 "goto-prev-hunk", _("Go to the previous hunk"), NULL);
1504 keybindings_set_item (kb_group, KB_GOTO_NEXT_HUNK, on_kb_goto_next_hunk, 0, 0,
1505 "goto-next-hunk", _("Go to the next hunk"), NULL);
1506 keybindings_set_item (kb_group, KB_UNDO_HUNK, on_kb_undo_hunk, 0, 0,
1507 "undo-hunk", _("Undo hunk at the cursor position"),
1508 G_undo_menu_item);
1509
1510 plugin_signal_connect (geany_plugin, NULL, "editor-notify", TRUE,
1511 G_CALLBACK (on_editor_notify), NULL);
1512 plugin_signal_connect (geany_plugin, NULL, "update-editor-menu", TRUE,
1513 G_CALLBACK (on_update_editor_menu), NULL);
1514 plugin_signal_connect (geany_plugin, NULL, "document-activate", TRUE,
1515 G_CALLBACK (on_document_activate), NULL);
1516 plugin_signal_connect (geany_plugin, NULL, "document-reload", TRUE,
1517 G_CALLBACK (on_document_activate), NULL);
1518 plugin_signal_connect (geany_plugin, NULL, "document-save", TRUE,
1519 G_CALLBACK (on_document_activate), NULL);
1520 plugin_signal_connect (geany_plugin, NULL, "geany-startup-complete", TRUE,
1521 G_CALLBACK (on_startup_complete), NULL);
1522
1523 if (main_is_realized ()) {
1524 /* update for the current document as we are loaded in the middle of a
1525 * session and so won't receive the :geany-startup-complete signal */
1526 on_startup_complete (NULL, NULL);
1527 }
1528 }
1529
1530 void
plugin_cleanup(void)1531 plugin_cleanup (void)
1532 {
1533 guint i = 0;
1534
1535 gtk_widget_destroy (G_undo_menu_item);
1536
1537 if (G_source_id) {
1538 g_source_remove (G_source_id);
1539 G_source_id = 0;
1540 }
1541 if (G_thread) {
1542 g_async_queue_push (G_queue, QUIT_THREAD_JOB); /* notify the thread */
1543 g_thread_join (G_thread);
1544 G_thread = NULL;
1545 g_async_queue_unref (G_queue);
1546 G_queue = NULL;
1547 }
1548 clear_cached_blob_contents ();
1549
1550 foreach_document (i) {
1551 release_resources (documents[i]->editor->sci);
1552 }
1553
1554 save_config ();
1555
1556 git_libgit2_shutdown ();
1557 }
1558
1559 /* --- configuration dialog --- */
1560
1561 typedef struct ConfigureWidgets ConfigureWidgets;
1562 struct ConfigureWidgets {
1563 GtkWidget *base;
1564 GtkWidget *monitoring_check;
1565 GtkWidget *added_color_button;
1566 GtkWidget *changed_color_button;
1567 GtkWidget *removed_color_button;
1568 };
1569
configure_widgets_free(ConfigureWidgets * cw)1570 static void configure_widgets_free (ConfigureWidgets *cw)
1571 {
1572 g_object_unref (cw->base);
1573 g_free (cw);
1574 }
1575
1576 static void
color_from_int(GdkColor * color,guint32 val)1577 color_from_int (GdkColor *color,
1578 guint32 val)
1579 {
1580 color->red = ((val & 0xff0000) >> 16) * 0x101;
1581 color->green = ((val & 0x00ff00) >> 8) * 0x101;
1582 color->blue = ((val & 0x0000ff) >> 0) * 0x101;
1583 }
1584
1585 static guint32
color_to_int(const GdkColor * color)1586 color_to_int (const GdkColor *color)
1587 {
1588 return (((color->red / 0x101) << 16) |
1589 ((color->green / 0x101) << 8) |
1590 ((color->blue / 0x101) << 0));
1591 }
1592
1593 static void
on_plugin_configure_response(GtkDialog * dialog,gint response,ConfigureWidgets * cw)1594 on_plugin_configure_response (GtkDialog *dialog,
1595 gint response,
1596 ConfigureWidgets *cw)
1597 {
1598 switch (response) {
1599 case GTK_RESPONSE_APPLY:
1600 case GTK_RESPONSE_OK: {
1601 guint i = 0;
1602 GdkColor color;
1603 GeanyDocument *doc = document_get_current ();
1604
1605 G_monitoring_enabled = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (cw->monitoring_check));
1606 gtk_color_button_get_color (GTK_COLOR_BUTTON (cw->added_color_button),
1607 &color);
1608 G_markers[MARKER_LINE_ADDED].color = color_to_int (&color);
1609 gtk_color_button_get_color (GTK_COLOR_BUTTON (cw->changed_color_button),
1610 &color);
1611 G_markers[MARKER_LINE_CHANGED].color = color_to_int (&color);
1612 gtk_color_button_get_color (GTK_COLOR_BUTTON (cw->removed_color_button),
1613 &color);
1614 G_markers[MARKER_LINE_REMOVED].color = color_to_int (&color);
1615
1616 /* update everything */
1617 foreach_document (i) {
1618 release_resources (documents[i]->editor->sci);
1619 }
1620 if (doc) {
1621 update_diff_push (doc, TRUE);
1622 }
1623 }
1624 }
1625 }
1626
1627 static gchar *
get_data_dir_path(const gchar * filename)1628 get_data_dir_path (const gchar *filename)
1629 {
1630 gchar *prefix = NULL;
1631 gchar *path;
1632
1633 #ifdef G_OS_WIN32
1634 prefix = g_win32_get_package_installation_directory_of_module (NULL);
1635 #elif defined(__APPLE__)
1636 if (g_getenv ("GEANY_PLUGINS_SHARE_PATH"))
1637 return g_build_filename (g_getenv ("GEANY_PLUGINS_SHARE_PATH"),
1638 PLUGIN, filename, NULL);
1639 #endif
1640 path = g_build_filename (prefix ? prefix : "", PLUGINDATADIR, filename, NULL);
1641 g_free (prefix);
1642 return path;
1643 }
1644
1645 GtkWidget *
plugin_configure(GtkDialog * dialog)1646 plugin_configure (GtkDialog *dialog)
1647 {
1648 GError *error = NULL;
1649 GtkWidget *base = NULL;
1650 GtkBuilder *builder = gtk_builder_new ();
1651 gchar *path = get_data_dir_path ("prefs.ui");
1652
1653 gtk_builder_set_translation_domain (builder, GETTEXT_PACKAGE);
1654 if (! gtk_builder_add_from_file (builder, path, &error)) {
1655 g_critical (_("Failed to load UI definition, please check your "
1656 "installation. The error was: %s"), error->message);
1657 g_error_free (error);
1658 } else {
1659 GdkColor color;
1660 ConfigureWidgets *cw = g_malloc (sizeof *cw);
1661 struct {
1662 const gchar *name;
1663 GtkWidget **ptr;
1664 } map[] = {
1665 { "base", &cw->base },
1666 { "monitoring-check", &cw->monitoring_check },
1667 { "added-color-button", &cw->added_color_button },
1668 { "changed-color-button", &cw->changed_color_button },
1669 { "removed-color-button", &cw->removed_color_button },
1670 };
1671 guint i;
1672
1673 for (i = 0; i < G_N_ELEMENTS (map); i++) {
1674 *map[i].ptr = GTK_WIDGET (gtk_builder_get_object (builder, map[i].name));
1675 }
1676
1677 gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (cw->monitoring_check),
1678 G_monitoring_enabled);
1679 color_from_int (&color, G_markers[MARKER_LINE_ADDED].color);
1680 gtk_color_button_set_color (GTK_COLOR_BUTTON (cw->added_color_button),
1681 &color);
1682 color_from_int (&color, G_markers[MARKER_LINE_CHANGED].color);
1683 gtk_color_button_set_color (GTK_COLOR_BUTTON (cw->changed_color_button),
1684 &color);
1685 color_from_int (&color, G_markers[MARKER_LINE_REMOVED].color);
1686 gtk_color_button_set_color (GTK_COLOR_BUTTON (cw->removed_color_button),
1687 &color);
1688
1689 base = g_object_ref_sink (cw->base);
1690
1691 g_signal_connect_data (dialog, "response",
1692 G_CALLBACK (on_plugin_configure_response),
1693 cw, (GClosureNotify) configure_widgets_free, 0);
1694 }
1695
1696 g_free (path);
1697 g_object_unref (builder);
1698
1699 return base;
1700 }
1701