1 /*
2  * Pair Tag Highlighter
3  *
4  * highlights matching opening/closing HTML tags
5  *
6  * Author:  Volodymyr Kononenko aka kvm
7  * Email:   vm@kononenko.ws
8  *
9  */
10 
11 #include "config.h"
12 #include <geanyplugin.h>
13 #include <string.h>
14 #include "Scintilla.h"  /* for the SCNotification struct */
15 #include "SciLexer.h"
16 
17 #define INDICATOR_TAGMATCH 9
18 #define MAX_TAG_NAME 64
19 
20 #define MATCHING_PAIR_COLOR     0x00ff00    /* green */
21 #define NONMATCHING_PAIR_COLOR  0xff0000    /* red */
22 #define EMPTY_TAG_COLOR         0xffff00    /* yellow */
23 
24 /* Keyboard Shortcut */
25 enum {
26   KB_MATCH_TAG,
27   KB_SELECT_TAG,
28   KB_COUNT
29 };
30 
31 /* These items are set by Geany before plugin_init() is called. */
32 GeanyPlugin     *geany_plugin;
33 GeanyData       *geany_data;
34 
35 /* Is needed for clearing highlighting after moving cursor out
36  * from the tag */
37 static gint highlightedBrackets[] = {0, 0, 0, 0};
38 static GtkWidget *goto_matching_tag = NULL;
39 static GtkWidget *select_matching_tag = NULL;
40 
41 PLUGIN_VERSION_CHECK(224)
42 
43 PLUGIN_SET_TRANSLATABLE_INFO(LOCALEDIR, GETTEXT_PACKAGE, _("Pair Tag Highlighter"),
44                             _("Finds and highlights matching opening/closing HTML tag"),
45                             "1.1", "Volodymyr Kononenko <vm@kononenko.ws>")
46 
47 
48 /* Searches tag brackets.
49  * direction variable shows sets search direction:
50  * TRUE  - to the right
51  * FALSE - to the left
52  * from the current cursor position to the start of the line.
53  */
findBracket(ScintillaObject * sci,gint position,gint endOfSearchPos,gchar searchedBracket,gchar breakBracket,gboolean direction)54 static gint findBracket(ScintillaObject *sci, gint position, gint endOfSearchPos,
55                         gchar searchedBracket, gchar breakBracket, gboolean direction)
56 {
57     gint foundBracket = -1;
58     gint pos;
59 
60     if(TRUE == direction)
61     {
62         /* search to the right */
63         for(pos=position; pos<=endOfSearchPos; pos++)
64         {
65             gchar charAtCurPosition = sci_get_char_at(sci, pos);
66             gchar charAtPrevPosition = sci_get_char_at(sci, pos-1);
67             gchar charAtNextPosition = sci_get_char_at(sci, pos+1);
68 
69             if(charAtCurPosition == searchedBracket) {
70                 if ('>' == searchedBracket) {
71                     if (('-' == charAtPrevPosition) || ('?' == charAtPrevPosition))
72                         continue;
73                 } else if ('<' == searchedBracket) {
74                     if ('?' == charAtNextPosition)
75                         continue;
76                 }
77                 foundBracket = pos;
78                 break;
79             } else if(charAtCurPosition == breakBracket) {
80                 if ('<' == breakBracket) {
81                     if ('?' == charAtNextPosition)
82                         continue;
83                 }
84                 break;
85             }
86         }
87     }
88     else
89     {
90         /* search to the left */
91         for(pos=position-1; pos>=endOfSearchPos; pos--)
92         {
93             gchar charAtCurPosition = sci_get_char_at(sci, pos);
94             gchar charAtPrevPosition = sci_get_char_at(sci, pos+1);
95             gchar charAtNextPosition = sci_get_char_at(sci, pos-1);
96 
97             if(charAtCurPosition == searchedBracket)
98             {
99                 if ('<' == searchedBracket) {
100                     if ('?' == charAtPrevPosition)
101                         continue;
102                 } else if ('>' == searchedBracket) {
103                     if (('-' == charAtNextPosition) || ('?' == charAtNextPosition))
104                         continue;
105                 }
106                 foundBracket = pos;
107                 break;
108             } else if(charAtCurPosition == breakBracket) {
109                 if ('>' == breakBracket) {
110                     if (('-' == charAtNextPosition) || ('?' == charAtNextPosition))
111                         continue;
112                 }
113                 break;
114             }
115         }
116     }
117 
118     return foundBracket;
119 }
120 
121 
rgb2bgr(gint color)122 static gint rgb2bgr(gint color)
123 {
124     guint r, g, b;
125 
126     r = color >> 16;
127     g = (0x00ff00 & color) >> 8;
128     b = (0x0000ff & color);
129 
130     color = (r | (g << 8) | (b << 16));
131 
132     return color;
133 }
134 
135 
highlight_tag(ScintillaObject * sci,gint openingBracket,gint closingBracket,gint color)136 static void highlight_tag(ScintillaObject *sci, gint openingBracket,
137                           gint closingBracket, gint color)
138 {
139     scintilla_send_message(sci, SCI_SETINDICATORCURRENT, INDICATOR_TAGMATCH, 0);
140     scintilla_send_message(sci, SCI_INDICSETSTYLE,
141                             INDICATOR_TAGMATCH, INDIC_ROUNDBOX);
142     scintilla_send_message(sci, SCI_INDICSETFORE, INDICATOR_TAGMATCH, rgb2bgr(color));
143     scintilla_send_message(sci, SCI_INDICSETALPHA, INDICATOR_TAGMATCH, 60);
144     scintilla_send_message(sci, SCI_INDICATORFILLRANGE,
145                             openingBracket, closingBracket-openingBracket+1);
146 }
147 
148 
highlight_matching_pair(ScintillaObject * sci)149 static void highlight_matching_pair(ScintillaObject *sci)
150 {
151     highlight_tag(sci, highlightedBrackets[0], highlightedBrackets[1],
152                   MATCHING_PAIR_COLOR);
153     highlight_tag(sci, highlightedBrackets[2], highlightedBrackets[3],
154                   MATCHING_PAIR_COLOR);
155 }
156 
157 
clear_previous_highlighting(ScintillaObject * sci,gint rangeStart,gint rangeEnd)158 static void clear_previous_highlighting(ScintillaObject *sci, gint rangeStart, gint rangeEnd)
159 {
160     scintilla_send_message(sci, SCI_SETINDICATORCURRENT, INDICATOR_TAGMATCH, 0);
161     scintilla_send_message(sci, SCI_INDICATORCLEARRANGE, rangeStart, rangeEnd-rangeStart+1);
162 }
163 
164 
is_tag_self_closing(ScintillaObject * sci,gint closingBracket)165 static gboolean is_tag_self_closing(ScintillaObject *sci, gint closingBracket)
166 {
167     gboolean isTagSelfClosing = FALSE;
168     gchar charBeforeBracket = sci_get_char_at(sci, closingBracket-1);
169 
170     if('/' == charBeforeBracket)
171         isTagSelfClosing = TRUE;
172     return isTagSelfClosing;
173 }
174 
175 
is_tag_empty(gchar * tagName)176 static gboolean is_tag_empty(gchar *tagName)
177 {
178     const char *emptyTags[] = {"area", "base", "br", "col", "embed",
179                          "hr", "img", "input", "keygen", "link", "meta",
180                          "param", "source", "track", "wbr", "!DOCTYPE"};
181     unsigned int i;
182 
183     g_return_val_if_fail(tagName != NULL, FALSE);
184 
185     for(i=0; i<(sizeof(emptyTags)/sizeof(emptyTags[0])); i++)
186     {
187         if(strcmp(tagName, emptyTags[i]) == 0)
188             return TRUE;
189     }
190 
191     return FALSE;
192 }
193 
194 
is_tag_opening(ScintillaObject * sci,gint openingBracket)195 static gboolean is_tag_opening(ScintillaObject *sci, gint openingBracket)
196 {
197     gboolean isTagOpening = TRUE;
198     gchar charAfterBracket = sci_get_char_at(sci, openingBracket+1);
199 
200     if('/' == charAfterBracket)
201         isTagOpening = FALSE;
202     return isTagOpening;
203 }
204 
205 
get_tag_name(ScintillaObject * sci,gint openingBracket,gint closingBracket,gboolean isTagOpening)206 static gchar *get_tag_name(ScintillaObject *sci, gint openingBracket, gint closingBracket,
207                     gboolean isTagOpening)
208 {
209     gint nameStart = openingBracket + (TRUE == isTagOpening ? 1 : 2);
210     gint nameEnd = nameStart;
211     gchar charAtCurPosition = sci_get_char_at(sci, nameStart);
212 
213     while(' ' != charAtCurPosition && '>' != charAtCurPosition &&
214         '\t' != charAtCurPosition && '\r' != charAtCurPosition && '\n' != charAtCurPosition)
215     {
216         charAtCurPosition = sci_get_char_at(sci, nameEnd);
217         nameEnd++;
218         if(nameEnd-nameStart > MAX_TAG_NAME)
219             break;
220     }
221     return nameEnd > nameStart ? sci_get_contents_range(sci, nameStart, nameEnd-1) : NULL;
222 }
223 
224 
findMatchingOpeningTag(ScintillaObject * sci,gchar * tagName,gint openingBracket)225 static void findMatchingOpeningTag(ScintillaObject *sci, gchar *tagName, gint openingBracket)
226 {
227     gint pos;
228     gint openingTagsCount = 0;
229     gint closingTagsCount = 1;
230 
231     for(pos=openingBracket; pos>0; pos--)
232     {
233         /* are we inside tag? */
234         gint lineNumber = sci_get_line_from_position(sci, pos);
235         gint lineStart = sci_get_position_from_line(sci, lineNumber);
236         gint matchingOpeningBracket = findBracket(sci, pos, lineStart, '<', '\0', FALSE);
237         gint matchingClosingBracket = findBracket(sci, pos, lineStart, '>', '\0', FALSE);
238 
239         if(-1 != matchingOpeningBracket && -1 != matchingClosingBracket
240             && (matchingClosingBracket > matchingOpeningBracket))
241         {
242             /* we are inside of some tag. Let us check what tag*/
243             gboolean isMatchingTagOpening = is_tag_opening(sci, matchingOpeningBracket);
244             gchar *matchingTagName = get_tag_name(sci, matchingOpeningBracket,
245                                                   matchingClosingBracket,
246                                                   isMatchingTagOpening);
247             if(matchingTagName && strcmp(tagName, matchingTagName) == 0)
248             {
249                 if(TRUE == isMatchingTagOpening)
250                     openingTagsCount++;
251                 else
252                     closingTagsCount++;
253             }
254             pos = matchingOpeningBracket+1;
255             g_free(matchingTagName);
256         }
257         /* Speed up search: if findBracket returns -1, that means start of line
258          * is reached. There is no need to go through the same positions again.
259          * Jump to the start of line */
260         else if(-1 == matchingOpeningBracket || -1 == matchingClosingBracket)
261         {
262             pos = lineStart;
263             continue;
264         }
265         if(openingTagsCount == closingTagsCount)
266         {
267             /* matching tag is found */
268             highlightedBrackets[2] = matchingOpeningBracket;
269             highlightedBrackets[3] = matchingClosingBracket;
270             highlight_matching_pair(sci);
271             return;
272         }
273     }
274     highlight_tag(sci, highlightedBrackets[0], highlightedBrackets[1],
275                   NONMATCHING_PAIR_COLOR);
276 }
277 
278 
findMatchingClosingTag(ScintillaObject * sci,gchar * tagName,gint closingBracket)279 static void findMatchingClosingTag(ScintillaObject *sci, gchar *tagName, gint closingBracket)
280 {
281     gint pos;
282     gint linesInDocument = sci_get_line_count(sci);
283     gint endOfDocument = sci_get_position_from_line(sci, linesInDocument);
284     gint openingTagsCount = 1;
285     gint closingTagsCount = 0;
286 
287     for(pos=closingBracket; pos<endOfDocument; pos++)
288     {
289         /* are we inside tag? */
290         gint lineNumber = sci_get_line_from_position(sci, pos);
291         gint lineEnd = sci_get_line_end_position(sci, lineNumber);
292         gint matchingOpeningBracket = findBracket(sci, pos, lineEnd, '<', '\0', TRUE);
293         gint matchingClosingBracket = findBracket(sci, pos, lineEnd, '>', '\0', TRUE);
294 
295         if(-1 != matchingOpeningBracket && -1 != matchingClosingBracket
296             && (matchingClosingBracket > matchingOpeningBracket))
297         {
298             /* we are inside of some tag. Let us check what tag*/
299             gboolean isMatchingTagOpening = is_tag_opening(sci, matchingOpeningBracket);
300             gchar *matchingTagName = get_tag_name(sci, matchingOpeningBracket,
301                                                   matchingClosingBracket,
302                                                   isMatchingTagOpening);
303             if(matchingTagName && strcmp(tagName, matchingTagName) == 0)
304             {
305                 if(TRUE == isMatchingTagOpening)
306                     openingTagsCount++;
307                 else
308                     closingTagsCount++;
309             }
310             pos = matchingClosingBracket;
311             g_free(matchingTagName);
312         }
313 
314         if(openingTagsCount == closingTagsCount)
315         {
316             /* matching tag is found */
317             highlightedBrackets[2] = matchingOpeningBracket;
318             highlightedBrackets[3] = matchingClosingBracket;
319             highlight_matching_pair(sci);
320             return;
321         }
322     }
323     highlight_tag(sci, highlightedBrackets[0], highlightedBrackets[1],
324                   NONMATCHING_PAIR_COLOR);
325 }
326 
327 
findMatchingTag(ScintillaObject * sci,gint openingBracket,gint closingBracket)328 static void findMatchingTag(ScintillaObject *sci, gint openingBracket, gint closingBracket)
329 {
330     gboolean isTagOpening = is_tag_opening(sci, openingBracket);
331     gchar *tagName = get_tag_name(sci, openingBracket, closingBracket, isTagOpening);
332 
333     if (!tagName)
334         return;
335 
336     if(is_tag_self_closing(sci, closingBracket) || is_tag_empty(tagName)) {
337         highlight_tag(sci, openingBracket, closingBracket, EMPTY_TAG_COLOR);
338     } else {
339         if(isTagOpening)
340             findMatchingClosingTag(sci, tagName, closingBracket);
341         else
342             findMatchingOpeningTag(sci, tagName, openingBracket);
343     }
344 
345     g_free(tagName);
346 }
347 
348 
run_tag_highlighter(ScintillaObject * sci)349 static void run_tag_highlighter(ScintillaObject *sci)
350 {
351     gint position = sci_get_current_position(sci);
352     gint lineNumber = sci_get_current_line(sci);
353     gint lineStart = sci_get_position_from_line(sci, lineNumber);
354     gint lineEnd = sci_get_line_end_position(sci, lineNumber);
355     gint openingBracket = findBracket(sci, position, lineStart, '<', '>', FALSE);
356     gint closingBracket = findBracket(sci, position, lineEnd, '>', '<', TRUE);
357     int i;
358 
359     if(-1 == openingBracket || -1 == closingBracket)
360     {
361         clear_previous_highlighting(sci, highlightedBrackets[0], highlightedBrackets[1]);
362         clear_previous_highlighting(sci, highlightedBrackets[2], highlightedBrackets[3]);
363         for(i=0; i<3; i++)
364             highlightedBrackets[i] = 0;
365         return;
366     }
367 
368     /* If the cursor jumps from one tag into another, clear
369      * previous highlighted tags*/
370     if(openingBracket != highlightedBrackets[0] ||
371         closingBracket != highlightedBrackets[1])
372     {
373         clear_previous_highlighting(sci, highlightedBrackets[0], highlightedBrackets[1]);
374         clear_previous_highlighting(sci, highlightedBrackets[2], highlightedBrackets[3]);
375     }
376 
377     /* Don't run search on empty brackets <> */
378     if (closingBracket - openingBracket > 1) {
379         highlightedBrackets[0] = openingBracket;
380         highlightedBrackets[1] = closingBracket;
381 
382         findMatchingTag(sci, openingBracket, closingBracket);
383     }
384 }
385 
386 
387 /* Notification handler for editor-notify */
on_editor_notify(GObject * obj,GeanyEditor * editor,SCNotification * nt,gpointer user_data)388 static gboolean on_editor_notify(GObject *obj, GeanyEditor *editor,
389                                 SCNotification *nt, gpointer user_data)
390 {
391     gint lexer;
392 
393     lexer = sci_get_lexer(editor->sci);
394     if((lexer != SCLEX_HTML) && (lexer != SCLEX_XML) && (lexer != SCLEX_PHPSCRIPT))
395     {
396         return FALSE;
397     }
398 
399     /* nmhdr is a structure containing information about the event */
400     switch (nt->nmhdr.code)
401     {
402         case SCN_UPDATEUI:
403             run_tag_highlighter(editor->sci);
404             break;
405     }
406 
407     /* returning FALSE to allow Geany processing the event */
408     return FALSE;
409 }
410 static void
select_or_match_tag(gboolean select)411 select_or_match_tag (gboolean select)
412 {
413     gint cur_line;
414     gint jump_line=-5, select_start=0, select_end=0;
415     GeanyDocument *doc = document_get_current();
416     if(highlightedBrackets[0] != highlightedBrackets[2]){
417         cur_line = sci_get_current_position(doc->editor->sci);
418         if(cur_line >= highlightedBrackets[0] && cur_line <= highlightedBrackets[1]){
419             if (!select){
420                 jump_line = highlightedBrackets[2];
421             }
422         }
423         else if(cur_line >= highlightedBrackets[2] && cur_line <= highlightedBrackets[3]){
424             if(!select){
425                 jump_line = highlightedBrackets[0];
426             }
427         }
428         if(select){
429             select_end = (highlightedBrackets[0] < highlightedBrackets[2])?highlightedBrackets[3]+1:highlightedBrackets[1]+1;
430             select_start = (highlightedBrackets[0] < highlightedBrackets[2])?highlightedBrackets[0]:highlightedBrackets[2];
431         }
432     }
433     if (select){
434         sci_set_selection_start(doc->editor->sci, select_start);
435         sci_set_selection_end(doc->editor->sci, select_end);
436     }
437     else if (jump_line >= 0){
438         sci_set_current_position(doc->editor->sci, jump_line, TRUE);
439     }
440 }
441 
442 
443 static void
on_goto_matching_tag(GtkWidget * widget,gpointer user_data)444 on_goto_matching_tag(GtkWidget *widget, gpointer user_data)
445 {
446   select_or_match_tag(FALSE);
447   return;
448 }
449 static void
on_select_matching_tag(GtkWidget * widget,gpointer user_data)450 on_select_matching_tag(GtkWidget *widget, gpointer user_data)
451 {
452   select_or_match_tag(TRUE);
453   return;
454 }
455 static void
on_editor_menu_popup(GObject * object,const gchar * word,gint pos,GeanyDocument * doc,gpointer user_data)456 on_editor_menu_popup (GObject       *object,
457                        const gchar   *word,
458                        gint           pos,
459                        GeanyDocument *doc,
460                        gpointer       user_data)
461 {
462 
463    if(DOC_VALID(doc) && (doc->file_type->id == GEANY_FILETYPES_HTML || doc->file_type->id == GEANY_FILETYPES_PHP || doc->file_type->id == GEANY_FILETYPES_XML))
464     {
465         gtk_widget_set_sensitive (goto_matching_tag, TRUE);
466         gtk_widget_set_sensitive (select_matching_tag, TRUE);
467         gtk_widget_show(select_matching_tag);
468         gtk_widget_show(goto_matching_tag);
469     }
470     else{
471         gtk_widget_set_sensitive (goto_matching_tag, FALSE);
472         gtk_widget_set_sensitive (select_matching_tag, FALSE);
473         gtk_widget_hide(select_matching_tag);
474         gtk_widget_hide(goto_matching_tag);
475     }
476 }
477 
478 
479 PluginCallback plugin_callbacks[] =
480 {
481     { "editor-notify", (GCallback) &on_editor_notify, FALSE, NULL },
482     { "update-editor-menu", (GCallback) &on_editor_menu_popup, FALSE, NULL },
483     { NULL, NULL, FALSE, NULL }
484 };
485 
486 
plugin_init(GeanyData * data)487 void plugin_init(GeanyData *data)
488 {
489     GeanyKeyGroup *kb_group;
490     goto_matching_tag = gtk_menu_item_new_with_label (_("Goto Matching XML Tag"));
491     select_matching_tag = gtk_menu_item_new_with_label (_("Select Matching XML Tag"));
492     g_signal_connect (goto_matching_tag, "activate",
493                     G_CALLBACK (on_goto_matching_tag), NULL);
494     g_signal_connect (select_matching_tag, "activate",
495                     G_CALLBACK (on_select_matching_tag), NULL);
496     gtk_container_add (GTK_CONTAINER (data->main_widgets->editor_menu),
497                      goto_matching_tag);
498     gtk_container_add (GTK_CONTAINER (data->main_widgets->editor_menu),
499                      select_matching_tag);
500     kb_group = plugin_set_key_group (geany_plugin, PLUGIN, KB_COUNT, NULL);
501     keybindings_set_item (kb_group, KB_MATCH_TAG, NULL,
502                         0, 0, "goto_matching_tag", _("Go To Matching Tag"), goto_matching_tag);
503     keybindings_set_item (kb_group, KB_SELECT_TAG, NULL,
504                         0, 0, "select_matching_tag", _("Select To Matching Tag"), select_matching_tag);
505 }
506 
507 
plugin_cleanup(void)508 void plugin_cleanup(void)
509 {
510     GeanyDocument *doc = document_get_current();
511 
512     if (doc)
513     {
514         clear_previous_highlighting(doc->editor->sci, highlightedBrackets[0], highlightedBrackets[1]);
515         clear_previous_highlighting(doc->editor->sci, highlightedBrackets[2], highlightedBrackets[3]);
516     }
517 }
518