1 /* ide-highlight-engine.c
2  *
3  * Copyright 2015-2019 Christian Hergert <christian@hergert.me>
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  * SPDX-License-Identifier: GPL-3.0-or-later
19  */
20 
21 #define G_LOG_DOMAIN "ide-highlight-engine"
22 
23 #include "config.h"
24 
25 #include <dazzle.h>
26 #include <glib/gi18n.h>
27 #include <gtksourceview/gtksource.h>
28 #include <libide-plugins.h>
29 #include <string.h>
30 
31 #include "ide-buffer.h"
32 #include "ide-buffer-private.h"
33 #include "ide-highlight-engine.h"
34 #include "ide-highlight-index.h"
35 #include "ide-highlighter.h"
36 
37 #define HIGHLIGHT_QUANTA_USEC 5000
38 #define PRIVATE_TAG_PREFIX    "gb-private-tag"
39 
40 struct _IdeHighlightEngine
41 {
42   IdeObject            parent_instance;
43 
44   GWeakRef             buffer_wref;
45 
46   DzlSignalGroup      *signal_group;
47   IdeHighlighter      *highlighter;
48   GSettings           *settings;
49 
50   IdeExtensionAdapter *extension;
51 
52   GtkTextMark         *invalid_begin;
53   GtkTextMark         *invalid_end;
54 
55   GSList              *private_tags;
56   GSList              *public_tags;
57 
58   gint64               quanta_expiration;
59 
60   guint                work_timeout;
61 
62   guint                enabled : 1;
63 };
64 
65 G_DEFINE_FINAL_TYPE (IdeHighlightEngine, ide_highlight_engine, IDE_TYPE_OBJECT)
66 
67 enum {
68   PROP_0,
69   PROP_BUFFER,
70   PROP_HIGHLIGHTER,
71   LAST_PROP
72 };
73 
74 static GParamSpec *properties [LAST_PROP];
75 
76 static gboolean
get_invalidation_area(GtkTextIter * begin,GtkTextIter * end)77 get_invalidation_area (GtkTextIter *begin,
78                        GtkTextIter *end)
79 {
80   GtkTextIter begin_tmp;
81   GtkTextIter end_tmp;
82 
83   g_assert (begin != NULL);
84   g_assert (end != NULL);
85 
86   /*
87    * Move to the beginning of line.We dont use gtk_text_iter_backward_line
88    * because if begin is at the beginning of the line we dont want to
89    * move to the previous line
90    */
91   gtk_text_iter_set_line_offset (begin, 0);
92 
93   /* Move to the beginning of the next line. */
94   gtk_text_iter_forward_line (end);
95 
96   /* Save the original locations. We will need them down the line. */
97   begin_tmp = *begin;
98   end_tmp = *end;
99 
100   /*
101    * Fordward begin iter character by character until:
102    * - We reach a non space character
103    * - We reach end iter
104    */
105   while (g_unichar_isspace (gtk_text_iter_get_char (begin)) &&
106          gtk_text_iter_compare (begin, &end_tmp) < 0)
107     gtk_text_iter_forward_char (begin);
108 
109 
110   /*
111    * If after moving forward the begin iter, we reached the end iter,
112    * there is no need to play with the end iter.
113    */
114   if (gtk_text_iter_compare (begin, end) < 0)
115     {
116       /*
117        * Backward end iter character by character until:
118        * - We reach a non space character
119        * - We reach begin iter
120        */
121       while (g_unichar_isspace (gtk_text_iter_get_char (end)) &&
122              gtk_text_iter_compare (end, &begin_tmp) > 0)
123         gtk_text_iter_backward_char (end);
124 
125       /*
126        * If we found the character we are looking for then move one
127        * character forward in order to include it as the last
128        * character of the begin - end range.
129        */
130       if (gtk_text_iter_compare (end, &end_tmp) < 0)
131         gtk_text_iter_forward_char (end);
132     }
133 
134   return gtk_text_iter_compare (begin, end) < 0;
135 }
136 
137 static void
sync_tag_style(GtkSourceStyleScheme * style_scheme,GtkTextTag * tag)138 sync_tag_style (GtkSourceStyleScheme *style_scheme,
139                 GtkTextTag           *tag)
140 {
141   g_autofree gchar *foreground = NULL;
142   g_autofree gchar *background = NULL;
143   g_autofree gchar *tag_name = NULL;
144   gchar *style_name = NULL;
145   const gchar *colon;
146   GtkSourceStyle *style;
147   gboolean foreground_set = FALSE;
148   gboolean background_set = FALSE;
149   gboolean bold = FALSE;
150   gboolean bold_set = FALSE;
151   gboolean underline = FALSE;
152   gboolean underline_set = FALSE;
153   gboolean italic = FALSE;
154   gboolean italic_set = FALSE;
155   gsize tag_name_len;
156   gsize prefix_len;
157 
158   g_object_set (tag,
159                 "foreground-set", FALSE,
160                 "background-set", FALSE,
161                 "weight-set", FALSE,
162                 "underline-set", FALSE,
163                 "style-set", FALSE,
164                 NULL);
165 
166   g_object_get (tag, "name", &tag_name, NULL);
167 
168   if (tag_name == NULL || style_scheme == NULL)
169     return;
170 
171   prefix_len = strlen (PRIVATE_TAG_PREFIX);
172   tag_name_len = strlen (tag_name);
173   style_name = tag_name;
174 
175   /*
176    * Check if this is a private tag.A tag is private if it starts with
177    * PRIVATE_TAG_PREFIX "gb-private-tag".
178    * ex: gb-private-tag:c:boolean
179    * If the tag is private extract the original style name by moving the string
180    * strlen (PRIVATE_TAG_PREFIX) + 1 (the colon) characters.
181    */
182   if (tag_name_len > prefix_len && memcmp (tag_name, PRIVATE_TAG_PREFIX, prefix_len) == 0)
183     style_name = tag_name + prefix_len + 1;
184 
185   style = gtk_source_style_scheme_get_style (style_scheme, style_name);
186   if (style == NULL && (colon = strchr (style_name, ':')))
187     {
188       gchar defname[64];
189       g_snprintf (defname, sizeof defname, "def%s", colon);
190       style = gtk_source_style_scheme_get_style (style_scheme, defname);
191       if (style == NULL)
192         return;
193     }
194 
195   g_object_get (style,
196                 "background", &background,
197                 "background-set", &background_set,
198                 "foreground", &foreground,
199                 "foreground-set", &foreground_set,
200                 "bold", &bold,
201                 "bold-set", &bold_set,
202                 "pango-underline", &underline,
203                 "underline-set", &underline_set,
204                 "italic", &italic,
205                 "italic-set", &italic_set,
206                 NULL);
207 
208   if (background_set)
209     g_object_set (tag, "background", background, NULL);
210 
211   if (foreground_set)
212     g_object_set (tag, "foreground", foreground, NULL);
213 
214   if (bold_set && bold)
215     g_object_set (tag, "weight", PANGO_WEIGHT_BOLD, NULL);
216 
217   if (italic_set && italic)
218     g_object_set (tag, "style", PANGO_STYLE_ITALIC, NULL);
219 
220   if (underline_set && underline)
221     g_object_set (tag, "underline", PANGO_UNDERLINE_SINGLE, NULL);
222 }
223 
224 static GtkTextTag *
create_tag_from_style(IdeHighlightEngine * self,const gchar * style_name)225 create_tag_from_style (IdeHighlightEngine *self,
226                        const gchar        *style_name)
227 {
228   g_autoptr(IdeBuffer) buffer = NULL;
229   GtkSourceStyleScheme *style_scheme;
230   GtkTextTag *tag = NULL;
231 
232   g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
233   g_assert (style_name != NULL);
234 
235   buffer = g_weak_ref_get (&self->buffer_wref);
236 
237   if (buffer != NULL)
238     {
239       tag = gtk_text_buffer_create_tag (GTK_TEXT_BUFFER (buffer), style_name, NULL);
240       gtk_text_tag_set_priority (tag, 0);
241       style_scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (buffer));
242       sync_tag_style (style_scheme, tag);
243     }
244 
245   return tag;
246 }
247 
248 static GtkTextTag *
get_tag_from_style(IdeHighlightEngine * self,const gchar * style_name,gboolean private_tag)249 get_tag_from_style (IdeHighlightEngine *self,
250                     const gchar        *style_name,
251                     gboolean            private_tag)
252 {
253   g_autoptr(IdeBuffer) buffer = NULL;
254   g_autofree gchar *tmp_style_name = NULL;
255   GtkTextTagTable *tag_table;
256   GtkTextTag *tag;
257 
258   g_return_val_if_fail (IDE_IS_HIGHLIGHT_ENGINE (self), NULL);
259   g_return_val_if_fail (style_name != NULL, NULL);
260 
261   buffer = g_weak_ref_get (&self->buffer_wref);
262   if (buffer == NULL)
263     return NULL;
264 
265   /*
266    * If is private tag prepend the PRIVATE_TAG_PREFIX (gb-private-tag)
267    * to the string.This is used because tag name is the key used
268    * for saving tags in GtkTextTagTable and we dont want conflicts between
269    * public and private tags.
270    */
271   if (private_tag)
272     tmp_style_name = g_strdup_printf ("%s:%s", PRIVATE_TAG_PREFIX, style_name);
273   else
274     tmp_style_name = g_strdup (style_name);
275 
276   tag_table = gtk_text_buffer_get_tag_table (GTK_TEXT_BUFFER (buffer));
277   tag = gtk_text_tag_table_lookup (tag_table, tmp_style_name);
278 
279   if (tag == NULL)
280     {
281       tag = create_tag_from_style (self, tmp_style_name);
282       if (private_tag)
283         self->private_tags = g_slist_prepend (self->private_tags, tag);
284       else
285         self->public_tags = g_slist_prepend (self->public_tags, tag);
286     }
287 
288   return tag;
289 }
290 
291 
292 static IdeHighlightResult
ide_highlight_engine_apply_style(const GtkTextIter * begin,const GtkTextIter * end,const gchar * style_name)293 ide_highlight_engine_apply_style (const GtkTextIter *begin,
294                                   const GtkTextIter *end,
295                                   const gchar       *style_name)
296 {
297   IdeHighlightEngine *self;
298   GtkTextBuffer *buffer;
299   GtkTextTag *tag;
300 
301   buffer = gtk_text_iter_get_buffer (begin);
302   self = _ide_buffer_get_highlight_engine (IDE_BUFFER (buffer));
303   tag = get_tag_from_style (self, style_name, TRUE);
304 
305   gtk_text_buffer_apply_tag (buffer, tag, begin, end);
306 
307   if (g_get_monotonic_time () >= self->quanta_expiration)
308     return IDE_HIGHLIGHT_STOP;
309 
310   return IDE_HIGHLIGHT_CONTINUE;
311 }
312 
313 static gboolean
ide_highlight_engine_tick(IdeHighlightEngine * self)314 ide_highlight_engine_tick (IdeHighlightEngine *self)
315 {
316   g_autoptr(GtkTextBuffer) buffer = NULL;
317   GtkTextIter iter;
318   GtkTextIter invalid_begin;
319   GtkTextIter invalid_end;
320   GSList *tags_iter;
321 
322   IDE_PROBE;
323 
324   g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
325   g_assert (self->highlighter != NULL);
326   g_assert (self->invalid_begin != NULL);
327   g_assert (self->invalid_end != NULL);
328 
329   buffer = g_weak_ref_get (&self->buffer_wref);
330   if (buffer == NULL)
331     return G_SOURCE_REMOVE;
332 
333   self->quanta_expiration = g_get_monotonic_time () + HIGHLIGHT_QUANTA_USEC;
334 
335   gtk_text_buffer_get_iter_at_mark (buffer, &invalid_begin, self->invalid_begin);
336   gtk_text_buffer_get_iter_at_mark (buffer, &invalid_end, self->invalid_end);
337 
338   IDE_TRACE_MSG ("Highlight Range [%u:%u,%u:%u] (%s)",
339                  gtk_text_iter_get_line (&invalid_begin),
340                  gtk_text_iter_get_line_offset (&invalid_begin),
341                  gtk_text_iter_get_line (&invalid_end),
342                  gtk_text_iter_get_line_offset (&invalid_end),
343                  G_OBJECT_TYPE_NAME (self->highlighter));
344 
345   if (gtk_text_iter_compare (&invalid_begin, &invalid_end) >= 0)
346     IDE_GOTO (up_to_date);
347 
348   /* Clear all our tags */
349   for (tags_iter = self->private_tags; tags_iter; tags_iter = tags_iter->next)
350     gtk_text_buffer_remove_tag (buffer,
351                                 GTK_TEXT_TAG (tags_iter->data),
352                                 &invalid_begin,
353                                 &invalid_end);
354 
355   iter = invalid_begin;
356 
357   ide_highlighter_update (self->highlighter, ide_highlight_engine_apply_style,
358                           &invalid_begin, &invalid_end, &iter);
359 
360   if (gtk_text_iter_compare (&iter, &invalid_end) >= 0)
361     IDE_GOTO (up_to_date);
362 
363   /* Stop processing until further instruction if no movement was made */
364   if (gtk_text_iter_equal (&iter, &invalid_begin))
365     return G_SOURCE_REMOVE;
366 
367   gtk_text_buffer_move_mark (buffer, self->invalid_begin, &iter);
368 
369   return G_SOURCE_CONTINUE;
370 
371 up_to_date:
372   gtk_text_buffer_get_start_iter (buffer, &iter);
373   gtk_text_buffer_move_mark (buffer, self->invalid_begin, &iter);
374   gtk_text_buffer_move_mark (buffer, self->invalid_end, &iter);
375 
376   return G_SOURCE_REMOVE;
377 }
378 
379 static gboolean
ide_highlight_engine_work_timeout_handler(gpointer data)380 ide_highlight_engine_work_timeout_handler (gpointer data)
381 {
382   IdeHighlightEngine *self = data;
383 
384   g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
385 
386   if (self->enabled)
387     {
388       if (ide_highlight_engine_tick (self))
389         return G_SOURCE_CONTINUE;
390     }
391 
392   self->work_timeout = 0;
393 
394   return G_SOURCE_REMOVE;
395 }
396 
397 static void
ide_highlight_engine_queue_work(IdeHighlightEngine * self)398 ide_highlight_engine_queue_work (IdeHighlightEngine *self)
399 {
400   g_autoptr(GtkTextBuffer) buffer = NULL;
401 
402   g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
403 
404   buffer = g_weak_ref_get (&self->buffer_wref);
405   if (self->highlighter == NULL || buffer == NULL || self->work_timeout != 0)
406     return;
407 
408   /*
409    * NOTE: It would be really nice if we could use the GdkFrameClock here to
410    *       drive the next update instead of a timeout. It's possible that our
411    *       callback could get scheduled right before the frame processing would
412    *       begin. However, since that gets driven by something like a Wayland
413    *       callback, it won't yet be scheduled. So instead our function gets
414    *       called and we potentially cause a frame to drop.
415    */
416 
417   self->work_timeout = gdk_threads_add_idle_full (G_PRIORITY_LOW + 1,
418                                                   ide_highlight_engine_work_timeout_handler,
419                                                   self,
420                                                   NULL);
421 }
422 
423 /**
424  * ide_highlight_engine_advance:
425  * @self: a #IdeHighlightEngine
426  *
427  * This function is useful for #IdeHighlighter implementations that need to
428  * asynchronously do work to process the highlighting.
429  *
430  * If they return from their update function without advancing, nothing will
431  * happen until they call this method to proceed.
432  *
433  * Since: 3.32
434  */
435 void
ide_highlight_engine_advance(IdeHighlightEngine * self)436 ide_highlight_engine_advance (IdeHighlightEngine *self)
437 {
438   g_return_if_fail (IDE_IS_HIGHLIGHT_ENGINE (self));
439 
440   ide_highlight_engine_queue_work (self);
441 }
442 
443 static gboolean
invalidate_and_highlight(IdeHighlightEngine * self,GtkTextIter * begin,GtkTextIter * end)444 invalidate_and_highlight (IdeHighlightEngine *self,
445                           GtkTextIter        *begin,
446                           GtkTextIter        *end)
447 {
448   GtkTextBuffer *text_buffer;
449 
450   g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
451   g_assert (begin != NULL);
452   g_assert (end != NULL);
453 
454   if (!self->enabled)
455     return FALSE;
456 
457   text_buffer = gtk_text_iter_get_buffer (begin);
458 
459   if (get_invalidation_area (begin, end))
460     {
461       GtkTextIter begin_tmp;
462       GtkTextIter end_tmp;
463 
464       gtk_text_buffer_get_iter_at_mark (text_buffer, &begin_tmp, self->invalid_begin);
465       gtk_text_buffer_get_iter_at_mark (text_buffer, &end_tmp, self->invalid_end);
466 
467       if (gtk_text_iter_equal (&begin_tmp, &end_tmp))
468         {
469           gtk_text_buffer_move_mark (text_buffer, self->invalid_begin, begin);
470           gtk_text_buffer_move_mark (text_buffer, self->invalid_end, end);
471         }
472       else
473         {
474           if (gtk_text_iter_compare (begin, &begin_tmp) < 0)
475             gtk_text_buffer_move_mark (text_buffer, self->invalid_begin, begin);
476           if (gtk_text_iter_compare (end, &end_tmp) > 0)
477             gtk_text_buffer_move_mark (text_buffer, self->invalid_end, end);
478         }
479 
480       ide_highlight_engine_queue_work (self);
481 
482       return TRUE;
483     }
484 
485   return FALSE;
486 }
487 
488 static void
ide_highlight_engine_reload(IdeHighlightEngine * self)489 ide_highlight_engine_reload (IdeHighlightEngine *self)
490 {
491   g_autoptr(GtkTextBuffer) buffer = NULL;
492   GtkTextIter begin;
493   GtkTextIter end;
494   GSList *iter;
495 
496   IDE_ENTRY;
497 
498   g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
499 
500   dzl_clear_source (&self->work_timeout);
501 
502   buffer = g_weak_ref_get (&self->buffer_wref);
503   if (buffer == NULL)
504     IDE_EXIT;
505 
506   gtk_text_buffer_get_bounds (buffer, &begin, &end);
507 
508   /*
509    * Invalidate the whole buffer.
510    */
511   gtk_text_buffer_move_mark (buffer, self->invalid_begin, &begin);
512   gtk_text_buffer_move_mark (buffer, self->invalid_end, &end);
513 
514   /*
515    * Remove our highlight tags from the buffer.
516    */
517   for (iter = self->private_tags; iter; iter = iter->next)
518     gtk_text_buffer_remove_tag (buffer, iter->data, &begin, &end);
519   g_clear_pointer (&self->private_tags, g_slist_free);
520 
521   for (iter = self->public_tags; iter; iter = iter->next)
522     gtk_text_buffer_remove_tag (buffer, iter->data, &begin, &end);
523   g_clear_pointer (&self->public_tags, g_slist_free);
524 
525   if (self->highlighter == NULL)
526     IDE_EXIT;
527 
528   ide_highlight_engine_queue_work (self);
529 
530   IDE_EXIT;
531 }
532 
533 static void
ide_highlight_engine_set_highlighter(IdeHighlightEngine * self,IdeHighlighter * highlighter)534 ide_highlight_engine_set_highlighter (IdeHighlightEngine *self,
535                                       IdeHighlighter     *highlighter)
536 {
537   g_return_if_fail (IDE_IS_HIGHLIGHT_ENGINE (self));
538   g_return_if_fail (!highlighter || IDE_IS_HIGHLIGHTER (highlighter));
539 
540   if (g_set_object (&self->highlighter, highlighter))
541     {
542       if (highlighter != NULL)
543         {
544           IDE_HIGHLIGHTER_GET_IFACE (highlighter)->set_engine (highlighter, self);
545           ide_highlighter_load (highlighter);
546         }
547 
548       ide_highlight_engine_reload (self);
549       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_HIGHLIGHTER]);
550     }
551 }
552 
553 static void
ide_highlight_engine__buffer_insert_text_cb(IdeHighlightEngine * self,GtkTextIter * location,gchar * text,gint len,IdeBuffer * buffer)554 ide_highlight_engine__buffer_insert_text_cb (IdeHighlightEngine *self,
555                                              GtkTextIter        *location,
556                                              gchar              *text,
557                                              gint                len,
558                                              IdeBuffer          *buffer)
559 {
560   GtkTextIter begin;
561   GtkTextIter end;
562 
563   IDE_ENTRY;
564 
565   g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
566   g_assert (location);
567   g_assert (text);
568   g_assert (IDE_IS_BUFFER (buffer));
569 
570   if (!self->enabled)
571     IDE_EXIT;
572 
573   /*
574    * Backward the begin iter len characters from location
575    * (location points to the end of the string) in order to get
576    * the iter position where our inserted text was started.
577    */
578   begin = *location;
579   gtk_text_iter_backward_chars (&begin, g_utf8_strlen (text, len));
580 
581   end = *location;
582 
583   invalidate_and_highlight (self, &begin, &end);
584 
585   IDE_EXIT;
586 }
587 
588 static void
ide_highlight_engine__buffer_delete_range_cb(IdeHighlightEngine * self,GtkTextIter * range_begin,GtkTextIter * range_end,IdeBuffer * buffer)589 ide_highlight_engine__buffer_delete_range_cb (IdeHighlightEngine *self,
590                                               GtkTextIter        *range_begin,
591                                               GtkTextIter        *range_end,
592                                               IdeBuffer          *buffer)
593 {
594   GtkTextIter begin;
595   GtkTextIter end;
596 
597   IDE_ENTRY;
598 
599   g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
600   g_assert (range_begin);
601   g_assert (IDE_IS_BUFFER (buffer));
602 
603   if (!self->enabled)
604     IDE_EXIT;
605 
606   /*
607    * No need to use the range_end since everything that
608    * was after range_end will now be after range_begin
609    */
610   begin = *range_begin;
611   end = *range_begin;
612 
613   invalidate_and_highlight (self, &begin, &end);
614 
615   IDE_EXIT;
616 }
617 
618 static void
ide_highlight_engine__notify_language_cb(IdeHighlightEngine * self,GParamSpec * pspec,IdeBuffer * buffer)619 ide_highlight_engine__notify_language_cb (IdeHighlightEngine *self,
620                                           GParamSpec         *pspec,
621                                           IdeBuffer          *buffer)
622 {
623   g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
624   g_assert (IDE_IS_BUFFER (buffer));
625 
626   if (self->extension != NULL)
627     {
628       GtkSourceLanguage *language;
629       const gchar *lang_id = NULL;
630 
631       if ((language = gtk_source_buffer_get_language (GTK_SOURCE_BUFFER (buffer))))
632         lang_id = gtk_source_language_get_id (language);
633 
634       ide_extension_adapter_set_value (self->extension, lang_id);
635     }
636 }
637 
638 static void
ide_highlight_engine__notify_style_scheme_cb(IdeHighlightEngine * self,GParamSpec * pspec,IdeBuffer * buffer)639 ide_highlight_engine__notify_style_scheme_cb (IdeHighlightEngine *self,
640                                               GParamSpec         *pspec,
641                                               IdeBuffer          *buffer)
642 {
643   GtkSourceStyleScheme *style_scheme;
644   GSList *iter;
645 
646   g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
647   g_assert (IDE_IS_BUFFER (buffer));
648 
649   style_scheme = gtk_source_buffer_get_style_scheme (GTK_SOURCE_BUFFER (buffer));
650 
651   for (iter = self->private_tags; iter; iter = iter->next)
652     sync_tag_style (style_scheme, iter->data);
653   for (iter = self->public_tags; iter; iter = iter->next)
654     sync_tag_style (style_scheme, iter->data);
655 }
656 
657 void
ide_highlight_engine_clear(IdeHighlightEngine * self)658 ide_highlight_engine_clear (IdeHighlightEngine *self)
659 {
660   g_autoptr(GtkTextBuffer) buffer = NULL;
661   GtkTextIter begin;
662   GtkTextIter end;
663 
664   g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
665 
666   buffer = g_weak_ref_get (&self->buffer_wref);
667 
668   if (buffer != NULL)
669     {
670       gtk_text_buffer_get_bounds (buffer, &begin, &end);
671 
672       for (const GSList *iter = self->public_tags; iter; iter = iter->next)
673         {
674           GtkTextTag *tag = iter->data;
675           gtk_text_buffer_remove_tag (buffer, tag, &begin, &end);
676         }
677     }
678 }
679 
680 static void
ide_highlight_engine__bind_buffer_cb(IdeHighlightEngine * self,IdeBuffer * buffer,DzlSignalGroup * group)681 ide_highlight_engine__bind_buffer_cb (IdeHighlightEngine *self,
682                                       IdeBuffer          *buffer,
683                                       DzlSignalGroup     *group)
684 {
685   GtkTextBuffer *text_buffer = (GtkTextBuffer *)buffer;
686   GtkTextIter begin;
687   GtkTextIter end;
688 
689   IDE_ENTRY;
690 
691   g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
692   g_assert (IDE_IS_BUFFER (buffer));
693   g_assert (DZL_IS_SIGNAL_GROUP (group));
694   g_assert (self->invalid_begin == NULL);
695   g_assert (self->invalid_end == NULL);
696 
697   g_weak_ref_set (&self->buffer_wref, buffer);
698 
699   gtk_text_buffer_get_bounds (text_buffer, &begin, &end);
700 
701   self->invalid_begin = gtk_text_buffer_create_mark (text_buffer, NULL, &begin, TRUE);
702   self->invalid_end = gtk_text_buffer_create_mark (text_buffer, NULL, &end, FALSE);
703 
704   /* We can hold a full reference to the text marks, without
705    * taking a reference to the buffer. We want to avoid a reference
706    * to the buffer for cyclic reasons.
707    */
708   g_object_ref (self->invalid_begin);
709   g_object_ref (self->invalid_end);
710 
711   ide_highlight_engine__notify_style_scheme_cb (self, NULL, buffer);
712   ide_highlight_engine__notify_language_cb (self, NULL, buffer);
713 
714   ide_highlight_engine_reload (self);
715 
716   IDE_EXIT;
717 }
718 
719 static void
ide_highlight_engine__unbind_buffer_cb(IdeHighlightEngine * self,DzlSignalGroup * group)720 ide_highlight_engine__unbind_buffer_cb (IdeHighlightEngine  *self,
721                                         DzlSignalGroup      *group)
722 {
723   g_autoptr(GtkTextBuffer) text_buffer = NULL;
724 
725   IDE_ENTRY;
726 
727   g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
728   g_assert (DZL_IS_SIGNAL_GROUP (group));
729 
730   text_buffer = g_weak_ref_get (&self->buffer_wref);
731 
732   dzl_clear_source (&self->work_timeout);
733 
734   if (text_buffer != NULL)
735     {
736       g_autoptr(GSList) private_tags = NULL;
737       g_autoptr(GSList) public_tags = NULL;
738       GtkTextTagTable *tag_table;
739       GtkTextIter begin;
740       GtkTextIter end;
741 
742       tag_table = gtk_text_buffer_get_tag_table (text_buffer);
743 
744       gtk_text_buffer_delete_mark (text_buffer, self->invalid_begin);
745       gtk_text_buffer_delete_mark (text_buffer, self->invalid_end);
746 
747       gtk_text_buffer_get_bounds (text_buffer, &begin, &end);
748 
749       private_tags = g_steal_pointer (&self->private_tags);
750       public_tags = g_steal_pointer (&self->public_tags);
751 
752       for (const GSList *iter = private_tags; iter; iter = iter->next)
753         {
754           gtk_text_buffer_remove_tag (text_buffer, iter->data, &begin, &end);
755           gtk_text_tag_table_remove (tag_table, iter->data);
756         }
757 
758       for (const GSList *iter = public_tags; iter; iter = iter->next)
759         {
760           gtk_text_buffer_remove_tag (text_buffer, iter->data, &begin, &end);
761           gtk_text_tag_table_remove (tag_table, iter->data);
762         }
763     }
764 
765   g_clear_pointer (&self->public_tags, g_slist_free);
766   g_clear_pointer (&self->private_tags, g_slist_free);
767 
768   g_clear_object (&self->invalid_begin);
769   g_clear_object (&self->invalid_end);
770 
771   IDE_EXIT;
772 }
773 
774 static void
ide_highlight_engine_set_buffer(IdeHighlightEngine * self,IdeBuffer * buffer)775 ide_highlight_engine_set_buffer (IdeHighlightEngine *self,
776                                  IdeBuffer          *buffer)
777 {
778   g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
779   g_assert (!buffer || GTK_IS_TEXT_BUFFER (buffer));
780 
781   /* We can get GtkSourceBuffer intermittently here. */
782   if (!buffer || IDE_IS_BUFFER (buffer))
783     {
784       dzl_signal_group_set_target (self->signal_group, buffer);
785       g_object_notify_by_pspec (G_OBJECT (self), properties [PROP_BUFFER]);
786     }
787 }
788 
789 static void
ide_highlight_engine_settings_changed(IdeHighlightEngine * self,const gchar * key,GSettings * settings)790 ide_highlight_engine_settings_changed (IdeHighlightEngine *self,
791                                        const gchar        *key,
792                                        GSettings          *settings)
793 {
794   g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
795   g_assert (G_IS_SETTINGS (settings));
796 
797   if (g_settings_get_boolean (settings, "semantic-highlighting"))
798     {
799       self->enabled = TRUE;
800       ide_highlight_engine_rebuild (self);
801     }
802   else
803     {
804       self->enabled = FALSE;
805       ide_highlight_engine_clear (self);
806     }
807 }
808 
809 static void
ide_highlight_engine__notify_extension(IdeHighlightEngine * self,GParamSpec * pspec,IdeExtensionAdapter * adapter)810 ide_highlight_engine__notify_extension (IdeHighlightEngine  *self,
811                                         GParamSpec          *pspec,
812                                         IdeExtensionAdapter *adapter)
813 {
814   IdeHighlighter *highlighter;
815 
816   g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
817   g_assert (IDE_IS_EXTENSION_ADAPTER (adapter));
818 
819   highlighter = ide_extension_adapter_get_extension (adapter);
820   g_return_if_fail (!highlighter || IDE_IS_HIGHLIGHTER (highlighter));
821 
822   ide_highlight_engine_set_highlighter (self, highlighter);
823 }
824 
825 static void
ide_highlight_engine_parent_set(IdeObject * object,IdeObject * parent)826 ide_highlight_engine_parent_set (IdeObject *object,
827                                  IdeObject *parent)
828 {
829   IdeHighlightEngine *self = (IdeHighlightEngine *)object;
830 
831   g_assert (IDE_IS_HIGHLIGHT_ENGINE (self));
832   g_assert (!parent || IDE_IS_OBJECT (parent));
833 
834   if (parent == NULL)
835     {
836       g_clear_object (&self->extension);
837       return;
838     }
839 
840   self->extension = ide_extension_adapter_new (IDE_OBJECT (self),
841                                                NULL,
842                                                IDE_TYPE_HIGHLIGHTER,
843                                                "Highlighter-Languages",
844                                                NULL);
845   g_signal_connect_object (self->extension,
846                            "notify::extension",
847                            G_CALLBACK (ide_highlight_engine__notify_extension),
848                            self,
849                            G_CONNECT_SWAPPED);
850 }
851 
852 static void
ide_highlight_engine_dispose(GObject * object)853 ide_highlight_engine_dispose (GObject *object)
854 {
855   IdeHighlightEngine *self = (IdeHighlightEngine *)object;
856 
857   g_weak_ref_set (&self->buffer_wref, NULL);
858   g_clear_object (&self->signal_group);
859   g_clear_object (&self->extension);
860   g_clear_object (&self->highlighter);
861   g_clear_object (&self->settings);
862 
863   G_OBJECT_CLASS (ide_highlight_engine_parent_class)->dispose (object);
864 }
865 
866 static void
ide_highlight_engine_finalize(GObject * object)867 ide_highlight_engine_finalize (GObject *object)
868 {
869   IdeHighlightEngine *self = (IdeHighlightEngine *)object;
870 
871   g_weak_ref_clear (&self->buffer_wref);
872 
873   G_OBJECT_CLASS (ide_highlight_engine_parent_class)->finalize (object);
874 }
875 
876 static void
ide_highlight_engine_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)877 ide_highlight_engine_get_property (GObject    *object,
878                                    guint       prop_id,
879                                    GValue     *value,
880                                    GParamSpec *pspec)
881 {
882   IdeHighlightEngine *self = IDE_HIGHLIGHT_ENGINE (object);
883 
884   switch (prop_id)
885     {
886     case PROP_BUFFER:
887       g_value_set_object (value, ide_highlight_engine_get_buffer (self));
888       break;
889 
890     case PROP_HIGHLIGHTER:
891       g_value_set_object (value, ide_highlight_engine_get_highlighter (self));
892       break;
893 
894     default:
895       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
896     }
897 }
898 
899 static void
ide_highlight_engine_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)900 ide_highlight_engine_set_property (GObject      *object,
901                                    guint         prop_id,
902                                    const GValue *value,
903                                    GParamSpec   *pspec)
904 {
905   IdeHighlightEngine *self = IDE_HIGHLIGHT_ENGINE (object);
906 
907   switch (prop_id)
908     {
909     case PROP_BUFFER:
910       ide_highlight_engine_set_buffer (self, g_value_get_object (value));
911       break;
912 
913     default:
914       G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
915     }
916 }
917 
918 static void
ide_highlight_engine_class_init(IdeHighlightEngineClass * klass)919 ide_highlight_engine_class_init (IdeHighlightEngineClass *klass)
920 {
921   GObjectClass *object_class = G_OBJECT_CLASS (klass);
922   IdeObjectClass *i_object_class = IDE_OBJECT_CLASS (klass);
923 
924   object_class->dispose = ide_highlight_engine_dispose;
925   object_class->finalize = ide_highlight_engine_finalize;
926   object_class->get_property = ide_highlight_engine_get_property;
927   object_class->set_property = ide_highlight_engine_set_property;
928 
929   i_object_class->parent_set = ide_highlight_engine_parent_set;
930 
931   properties [PROP_BUFFER] =
932     g_param_spec_object ("buffer",
933                          "Buffer",
934                          "The buffer to highlight.",
935                          IDE_TYPE_BUFFER,
936                          (G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
937 
938   properties [PROP_HIGHLIGHTER] =
939     g_param_spec_object ("highlighter",
940                          "Highlighter",
941                          "The highlighter to use for type information.",
942                          IDE_TYPE_HIGHLIGHTER,
943                          (G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
944 
945   g_object_class_install_properties (object_class, LAST_PROP, properties);
946 }
947 
948 static void
ide_highlight_engine_init(IdeHighlightEngine * self)949 ide_highlight_engine_init (IdeHighlightEngine *self)
950 {
951   g_weak_ref_init (&self->buffer_wref, NULL);
952 
953   self->settings = g_settings_new ("org.gnome.builder.code-insight");
954   self->enabled = g_settings_get_boolean (self->settings, "semantic-highlighting");
955   self->signal_group = dzl_signal_group_new (IDE_TYPE_BUFFER);
956 
957   dzl_signal_group_connect_object (self->signal_group,
958                                    "insert-text",
959                                    G_CALLBACK (ide_highlight_engine__buffer_insert_text_cb),
960                                    self,
961                                    G_CONNECT_SWAPPED | G_CONNECT_AFTER);
962 
963   dzl_signal_group_connect_object (self->signal_group,
964                                    "delete-range",
965                                    G_CALLBACK (ide_highlight_engine__buffer_delete_range_cb),
966                                    self,
967                                    G_CONNECT_SWAPPED | G_CONNECT_AFTER);
968 
969   dzl_signal_group_connect_object (self->signal_group,
970                                    "notify::language",
971                                    G_CALLBACK (ide_highlight_engine__notify_language_cb),
972                                    self,
973                                    G_CONNECT_SWAPPED);
974 
975   dzl_signal_group_connect_object (self->signal_group,
976                                    "notify::style-scheme",
977                                    G_CALLBACK (ide_highlight_engine__notify_style_scheme_cb),
978                                    self,
979                                    G_CONNECT_SWAPPED);
980 
981   g_signal_connect_object (self->signal_group,
982                            "bind",
983                            G_CALLBACK (ide_highlight_engine__bind_buffer_cb),
984                            self,
985                            G_CONNECT_SWAPPED);
986 
987   g_signal_connect_object (self->signal_group,
988                            "unbind",
989                            G_CALLBACK (ide_highlight_engine__unbind_buffer_cb),
990                            self,
991                            G_CONNECT_SWAPPED);
992 
993   g_signal_connect_object (self->settings,
994                            "changed::semantic-highlighting",
995                            G_CALLBACK (ide_highlight_engine_settings_changed),
996                            self,
997                            G_CONNECT_SWAPPED);
998 }
999 
1000 IdeHighlightEngine *
ide_highlight_engine_new(IdeBuffer * buffer)1001 ide_highlight_engine_new (IdeBuffer *buffer)
1002 {
1003   IdeHighlightEngine *self;
1004   IdeObjectBox *box;
1005 
1006   g_return_val_if_fail (IDE_IS_BUFFER (buffer), NULL);
1007 
1008   self = g_object_new (IDE_TYPE_HIGHLIGHT_ENGINE,
1009                        "buffer", buffer,
1010                        NULL);
1011 
1012   box = ide_object_box_from_object (G_OBJECT (buffer));
1013   ide_object_append (IDE_OBJECT (box), IDE_OBJECT (self));
1014 
1015   return g_steal_pointer (&self);
1016 }
1017 
1018 /**
1019  * ide_highlight_engine_get_highlighter:
1020  * @self: an #IdeHighlightEngine.
1021  *
1022  * Gets the IdeHighlightEngine:highlighter property.
1023  *
1024  * Returns: (transfer none): An #IdeHighlighter.
1025  *
1026  * Since: 3.32
1027  */
1028 IdeHighlighter *
ide_highlight_engine_get_highlighter(IdeHighlightEngine * self)1029 ide_highlight_engine_get_highlighter (IdeHighlightEngine *self)
1030 {
1031   g_return_val_if_fail (IDE_IS_HIGHLIGHT_ENGINE (self), NULL);
1032 
1033   return self->highlighter;
1034 }
1035 
1036 /**
1037  * ide_highlight_engine_get_buffer:
1038  * @self: an #IdeHighlightEngine.
1039  *
1040  * Gets the IdeHighlightEngine:buffer property.
1041  *
1042  * Returns: (transfer none): An #IdeBuffer.
1043  *
1044  * Since: 3.32
1045  */
1046 IdeBuffer *
ide_highlight_engine_get_buffer(IdeHighlightEngine * self)1047 ide_highlight_engine_get_buffer (IdeHighlightEngine *self)
1048 {
1049   g_autoptr(IdeBuffer) buffer = NULL;
1050 
1051   g_return_val_if_fail (IDE_IS_HIGHLIGHT_ENGINE (self), NULL);
1052 
1053   /* We don't need the "thread-safety" provided by GWeakRef here,
1054    * (where it gives us a new object reference). It is safe to
1055    * return a borrowed reference instead.
1056    */
1057   buffer = g_weak_ref_get (&self->buffer_wref);
1058   return buffer;
1059 }
1060 
1061 void
ide_highlight_engine_rebuild(IdeHighlightEngine * self)1062 ide_highlight_engine_rebuild (IdeHighlightEngine *self)
1063 {
1064   g_autoptr(GtkTextBuffer) buffer = NULL;
1065 
1066   IDE_ENTRY;
1067 
1068   g_return_if_fail (IDE_IS_HIGHLIGHT_ENGINE (self));
1069 
1070   buffer = g_weak_ref_get (&self->buffer_wref);
1071 
1072   if (buffer != NULL)
1073     {
1074       GtkTextIter begin;
1075       GtkTextIter end;
1076 
1077       gtk_text_buffer_get_bounds (buffer, &begin, &end);
1078       gtk_text_buffer_move_mark (buffer, self->invalid_begin, &begin);
1079       gtk_text_buffer_move_mark (buffer, self->invalid_end, &end);
1080 
1081       ide_highlight_engine_queue_work (self);
1082     }
1083 
1084   IDE_EXIT;
1085 }
1086 
1087 /**
1088  * ide_highlight_engine_invalidate:
1089  * @self: An #IdeHighlightEngine.
1090  * @begin: the beginning of the range to invalidate
1091  * @end: the end of the range to invalidate
1092  *
1093  * This function will extend the invalidated range of the buffer to include
1094  * the range of @begin to @end.
1095  *
1096  * The highlighter will be queued to interactively update the invalidated
1097  * region.
1098  *
1099  * Updating the invalidated region of the buffer may take some time, as it is
1100  * important that the highlighter does not block for more than 1-2 milliseconds
1101  * to avoid dropping frames.
1102  *
1103  * Since: 3.32
1104  */
1105 void
ide_highlight_engine_invalidate(IdeHighlightEngine * self,const GtkTextIter * begin,const GtkTextIter * end)1106 ide_highlight_engine_invalidate (IdeHighlightEngine *self,
1107                                  const GtkTextIter  *begin,
1108                                  const GtkTextIter  *end)
1109 {
1110   GtkTextBuffer *buffer;
1111   GtkTextIter mark_begin;
1112   GtkTextIter mark_end;
1113 
1114   IDE_ENTRY;
1115 
1116   g_return_if_fail (IDE_IS_HIGHLIGHT_ENGINE (self));
1117   g_return_if_fail (begin != NULL);
1118   g_return_if_fail (end != NULL);
1119 
1120   buffer = gtk_text_iter_get_buffer (begin);
1121 
1122   gtk_text_buffer_get_iter_at_mark (buffer, &mark_begin, self->invalid_begin);
1123   gtk_text_buffer_get_iter_at_mark (buffer, &mark_end, self->invalid_end);
1124 
1125   if (gtk_text_iter_equal (&mark_begin, &mark_end))
1126     {
1127       gtk_text_buffer_move_mark (buffer, self->invalid_begin, begin);
1128       gtk_text_buffer_move_mark (buffer, self->invalid_end, end);
1129     }
1130   else
1131     {
1132       if (gtk_text_iter_compare (begin, &mark_begin) < 0)
1133         gtk_text_buffer_move_mark (buffer, self->invalid_begin, begin);
1134       if (gtk_text_iter_compare (end, &mark_end) > 0)
1135         gtk_text_buffer_move_mark (buffer, self->invalid_end, end);
1136     }
1137 
1138   ide_highlight_engine_queue_work (self);
1139 
1140   IDE_EXIT;
1141 }
1142 
1143 /**
1144  * ide_highlight_engine_get_style:
1145  * @self: the #IdeHighlightEngine
1146  * @style_name: the name of the style to retrieve
1147  *
1148  * A #GtkTextTag for @style_name.
1149  *
1150  * Returns: (transfer none): a #GtkTextTag.
1151  *
1152  * Since: 3.32
1153  */
1154 GtkTextTag *
ide_highlight_engine_get_style(IdeHighlightEngine * self,const gchar * style_name)1155 ide_highlight_engine_get_style (IdeHighlightEngine *self,
1156                                 const gchar        *style_name)
1157 {
1158   return get_tag_from_style (self, style_name, FALSE);
1159 }
1160 
1161 void
ide_highlight_engine_pause(IdeHighlightEngine * self)1162 ide_highlight_engine_pause (IdeHighlightEngine *self)
1163 {
1164   g_return_if_fail (IDE_IS_HIGHLIGHT_ENGINE (self));
1165 
1166   dzl_signal_group_block (self->signal_group);
1167 }
1168 
1169 void
ide_highlight_engine_unpause(IdeHighlightEngine * self)1170 ide_highlight_engine_unpause (IdeHighlightEngine *self)
1171 {
1172   g_autoptr(IdeBuffer) buffer = NULL;
1173 
1174   g_return_if_fail (IDE_IS_HIGHLIGHT_ENGINE (self));
1175   g_return_if_fail (self->signal_group != NULL);
1176 
1177   dzl_signal_group_unblock (self->signal_group);
1178 
1179   buffer = g_weak_ref_get (&self->buffer_wref);
1180 
1181   if (buffer != NULL)
1182     {
1183       /* Notify of some blocked signals */
1184       ide_highlight_engine__notify_style_scheme_cb (self, NULL, buffer);
1185       ide_highlight_engine__notify_language_cb (self, NULL, buffer);
1186 
1187       ide_highlight_engine_reload (self);
1188     }
1189 }
1190