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