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