1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8; coding: utf-8 -*- */
2 /* ide-source-iter.c
3  * This file is part of GtkSourceView
4  *
5  * Copyright 2014 - Sébastien Wilmet <swilmet@gnome.org>
6  *
7  * GtkSourceView is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU Lesser General Public
9  * License as published by the Free Software Foundation; either
10  * version 2.1 of the License, or (at your option) any later version.
11  *
12  * GtkSourceView is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Lesser General Public License for more details.
16  *
17  * You should have received a copy of the GNU Lesser General Public
18  * License along with this library; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
20  */
21 
22 /* GtkTextIter functions. Contains forward/backward functions for word
23  * movements, with custom word boundaries that are used for word selection
24  * (double-click) and cursor movements (Ctrl+left, Ctrl+right, etc).  The
25  * initial idea was to use those word boundaries directly in GTK+, for all text
26  * widgets. But in the end only the GtkTextView::extend-selection signal has
27  * been added to be able to customize the boundaries for double- and
28  * triple-click (the ::move-cursor and ::delete-from-cursor signals were already
29  * present to customize boundaries for cursor movements). The GTK+ developers
30  * didn't want to change the word boundaries for text widgets. More information:
31  * https://mail.gnome.org/archives/gtk-devel-list/2014-September/msg00019.html
32  * https://bugzilla.gnome.org/show_bug.cgi?id=111503
33  */
34 
35 #include "config.h"
36 
37 #include "ide-source-iter.h"
38 
39 /* Go to the end of the next or current "full word". A full word is a group of
40  * non-blank chars.
41  * In other words, this function is the same as the 'E' Vim command.
42  *
43  * Examples ('|' is the iter position):
44  * "|---- abcd"   -> "----| abcd"
45  * "|  ---- abcd" -> "  ----| abcd"
46  * "--|-- abcd"   -> "----| abcd"
47  * "---- a|bcd"   -> "---- abcd|"
48  */
49 void
_ide_source_iter_forward_full_word_end(GtkTextIter * iter)50 _ide_source_iter_forward_full_word_end (GtkTextIter *iter)
51 {
52 	GtkTextIter pos;
53 	gboolean non_blank_found = FALSE;
54 
55 	/* It would be better to use gtk_text_iter_forward_visible_char(), but
56 	 * it doesn't exist. So move by cursor position instead, it should be
57 	 * equivalent here.
58 	 */
59 
60 	pos = *iter;
61 
62 	while (g_unichar_isspace (gtk_text_iter_get_char (&pos)))
63 	{
64 		gtk_text_iter_forward_visible_cursor_position (&pos);
65 	}
66 
67 	while (!gtk_text_iter_is_end (&pos) &&
68 	       !g_unichar_isspace (gtk_text_iter_get_char (&pos)))
69 	{
70 		non_blank_found = TRUE;
71 		gtk_text_iter_forward_visible_cursor_position (&pos);
72 	}
73 
74 	if (non_blank_found)
75 	{
76 		*iter = pos;
77 	}
78 }
79 
80 /* Symmetric of iter_forward_full_word_end(). */
81 void
_ide_source_iter_backward_full_word_start(GtkTextIter * iter)82 _ide_source_iter_backward_full_word_start (GtkTextIter *iter)
83 {
84 	GtkTextIter pos;
85 	GtkTextIter prev;
86 	gboolean non_blank_found = FALSE;
87 
88 	pos = *iter;
89 
90 	while (!gtk_text_iter_is_start (&pos))
91 	{
92 		prev = pos;
93 		gtk_text_iter_backward_visible_cursor_position (&prev);
94 
95 		if (!g_unichar_isspace (gtk_text_iter_get_char (&prev)))
96 		{
97 			break;
98 		}
99 
100 		pos = prev;
101 	}
102 
103 	while (!gtk_text_iter_is_start (&pos))
104 	{
105 		prev = pos;
106 		gtk_text_iter_backward_visible_cursor_position (&prev);
107 
108 		if (g_unichar_isspace (gtk_text_iter_get_char (&prev)))
109 		{
110 			break;
111 		}
112 
113 		non_blank_found = TRUE;
114 		pos = prev;
115 	}
116 
117 	if (non_blank_found)
118 	{
119 		*iter = pos;
120 	}
121 }
122 
123 gboolean
_ide_source_iter_starts_full_word(const GtkTextIter * iter)124 _ide_source_iter_starts_full_word (const GtkTextIter *iter)
125 {
126 	GtkTextIter prev = *iter;
127 
128 	if (gtk_text_iter_is_end (iter))
129 	{
130 		return FALSE;
131 	}
132 
133 	if (!gtk_text_iter_backward_visible_cursor_position (&prev))
134 	{
135 		return !g_unichar_isspace (gtk_text_iter_get_char (iter));
136 	}
137 
138 	return (g_unichar_isspace (gtk_text_iter_get_char (&prev)) &&
139 		!g_unichar_isspace (gtk_text_iter_get_char (iter)));
140 }
141 
142 gboolean
_ide_source_iter_ends_full_word(const GtkTextIter * iter)143 _ide_source_iter_ends_full_word (const GtkTextIter *iter)
144 {
145 	GtkTextIter prev = *iter;
146 
147 	if (!gtk_text_iter_backward_visible_cursor_position (&prev))
148 	{
149 		return FALSE;
150 	}
151 
152 	return (!g_unichar_isspace (gtk_text_iter_get_char (&prev)) &&
153 		(gtk_text_iter_is_end (iter) ||
154 		 g_unichar_isspace (gtk_text_iter_get_char (iter))));
155 }
156 
157 /* Extends the definition of a natural-language word used by Pango. The
158  * underscore is added to the possible characters of a natural-language word.
159  */
160 void
_ide_source_iter_forward_extra_natural_word_end(GtkTextIter * iter)161 _ide_source_iter_forward_extra_natural_word_end (GtkTextIter *iter)
162 {
163 	GtkTextIter next_word_end = *iter;
164 	GtkTextIter next_underscore_end = *iter;
165 	GtkTextIter *limit = NULL;
166 	gboolean found;
167 
168 	if (gtk_text_iter_forward_visible_word_end (&next_word_end))
169 	{
170 		limit = &next_word_end;
171 	}
172 
173 	found = gtk_text_iter_forward_search (iter,
174 					      "_",
175 					      GTK_TEXT_SEARCH_VISIBLE_ONLY | GTK_TEXT_SEARCH_TEXT_ONLY,
176 					      NULL,
177 					      &next_underscore_end,
178 					      limit);
179 
180 	if (found)
181 	{
182 		*iter = next_underscore_end;
183 	}
184 	else
185 	{
186 		*iter = next_word_end;
187 	}
188 
189 	while (TRUE)
190 	{
191 		if (gtk_text_iter_get_char (iter) == '_')
192 		{
193 			gtk_text_iter_forward_visible_cursor_position (iter);
194 		}
195 		else if (gtk_text_iter_starts_word (iter))
196 		{
197 			gtk_text_iter_forward_visible_word_end (iter);
198 		}
199 		else
200 		{
201 			break;
202 		}
203 	}
204 }
205 
206 /* Symmetric of iter_forward_extra_natural_word_end(). */
207 void
_ide_source_iter_backward_extra_natural_word_start(GtkTextIter * iter)208 _ide_source_iter_backward_extra_natural_word_start (GtkTextIter *iter)
209 {
210 	GtkTextIter prev_word_start = *iter;
211 	GtkTextIter prev_underscore_start = *iter;
212 	GtkTextIter *limit = NULL;
213 	gboolean found;
214 
215 	if (gtk_text_iter_backward_visible_word_start (&prev_word_start))
216 	{
217 		limit = &prev_word_start;
218 	}
219 
220 	found = gtk_text_iter_backward_search (iter,
221 					       "_",
222 					       GTK_TEXT_SEARCH_VISIBLE_ONLY | GTK_TEXT_SEARCH_TEXT_ONLY,
223 					       &prev_underscore_start,
224 					       NULL,
225 					       limit);
226 
227 	if (found)
228 	{
229 		*iter = prev_underscore_start;
230 	}
231 	else
232 	{
233 		*iter = prev_word_start;
234 	}
235 
236 	while (!gtk_text_iter_is_start (iter))
237 	{
238 		GtkTextIter prev = *iter;
239 		gtk_text_iter_backward_visible_cursor_position (&prev);
240 
241 		if (gtk_text_iter_get_char (&prev) == '_')
242 		{
243 			*iter = prev;
244 		}
245 		else if (gtk_text_iter_ends_word (iter))
246 		{
247 			gtk_text_iter_backward_visible_word_start (iter);
248 		}
249 		else
250 		{
251 			break;
252 		}
253 	}
254 }
255 
256 gboolean
_ide_source_iter_starts_extra_natural_word(const GtkTextIter * iter)257 _ide_source_iter_starts_extra_natural_word (const GtkTextIter *iter)
258 {
259 	gboolean starts_word;
260 	GtkTextIter prev;
261 
262 	starts_word = gtk_text_iter_starts_word (iter);
263 
264 	prev = *iter;
265 	if (!gtk_text_iter_backward_visible_cursor_position (&prev))
266 	{
267 		return starts_word || gtk_text_iter_get_char (iter) == '_';
268 	}
269 
270 	if (starts_word)
271 	{
272 		return gtk_text_iter_get_char (&prev) != '_';
273 	}
274 
275 	return (gtk_text_iter_get_char (iter) == '_' &&
276 		gtk_text_iter_get_char (&prev) != '_' &&
277 		!gtk_text_iter_ends_word (iter));
278 }
279 
280 gboolean
_ide_source_iter_ends_extra_natural_word(const GtkTextIter * iter)281 _ide_source_iter_ends_extra_natural_word (const GtkTextIter *iter)
282 {
283 	GtkTextIter prev;
284 	gboolean ends_word;
285 
286 	prev = *iter;
287 	if (!gtk_text_iter_backward_visible_cursor_position (&prev))
288 	{
289 		return FALSE;
290 	}
291 
292 	ends_word = gtk_text_iter_ends_word (iter);
293 
294 	if (gtk_text_iter_is_end (iter))
295 	{
296 		return ends_word || gtk_text_iter_get_char (&prev) == '_';
297 	}
298 
299 	if (ends_word)
300 	{
301 		return gtk_text_iter_get_char (iter) != '_';
302 	}
303 
304 	return (gtk_text_iter_get_char (&prev) == '_' &&
305 		gtk_text_iter_get_char (iter) != '_' &&
306 		!gtk_text_iter_starts_word (iter));
307 }
308 
309 /* Similar to gtk_text_iter_forward_visible_word_end, but with a custom
310  * definition of "word".
311  *
312  * It is normally the same word boundaries as in Vim. This function is the same
313  * as the 'e' command.
314  *
315  * With the custom word definition, a word can be:
316  * - a natural-language word as defined by Pango, plus the underscore. The
317  *   underscore is added because it is often used in programming languages.
318  * - a group of contiguous non-blank characters.
319  */
320 gboolean
_ide_source_iter_forward_visible_word_end(GtkTextIter * iter)321 _ide_source_iter_forward_visible_word_end (GtkTextIter *iter)
322 {
323 	GtkTextIter orig = *iter;
324 	GtkTextIter farthest = *iter;
325 	GtkTextIter next_word_end = *iter;
326 	GtkTextIter word_start;
327 
328 	/* 'farthest' is the farthest position that this function can return. Example:
329 	 * "|---- aaaa"  ->  "----| aaaa"
330 	 */
331 	_ide_source_iter_forward_full_word_end (&farthest);
332 
333 	/* Go to the next extra-natural word end. It can be farther than
334 	 * 'farthest':
335 	 * "|---- aaaa"  ->  "---- aaaa|"
336 	 *
337 	 * Or it can remain at the same place:
338 	 * "aaaa| ----"  ->  "aaaa| ----"
339 	 */
340 	_ide_source_iter_forward_extra_natural_word_end (&next_word_end);
341 
342 	if (gtk_text_iter_compare (&farthest, &next_word_end) < 0 ||
343 	    gtk_text_iter_equal (iter, &next_word_end))
344 	{
345 		*iter = farthest;
346 		goto end;
347 	}
348 
349 	/* From 'next_word_end', go to the previous extra-natural word start.
350 	 *
351 	 * Example 1:
352 	 * iter:          "ab|cd"
353 	 * next_word_end: "abcd|" -> the good one
354 	 * word_start:    "|abcd"
355 	 *
356 	 * Example 2:
357 	 * iter:          "| abcd()"
358 	 * next_word_end: " abcd|()" -> the good one
359 	 * word_start:    " |abcd()"
360 	 *
361 	 * Example 3:
362 	 * iter:          "abcd|()efgh"
363 	 * next_word_end: "abcd()efgh|"
364 	 * word_start:    "abcd()|efgh" -> the good one, at the end of the word "()".
365 	 */
366 	word_start = next_word_end;
367 	_ide_source_iter_backward_extra_natural_word_start (&word_start);
368 
369 	/* Example 1 */
370 	if (gtk_text_iter_compare (&word_start, iter) <= 0)
371 	{
372 		*iter = next_word_end;
373 	}
374 
375 	/* Example 2 */
376 	else if (_ide_source_iter_starts_full_word (&word_start))
377 	{
378 		*iter = next_word_end;
379 	}
380 
381 	/* Example 3 */
382 	else
383 	{
384 		*iter = word_start;
385 	}
386 
387 end:
388 	return !gtk_text_iter_equal (&orig, iter) && !gtk_text_iter_is_end (iter);
389 }
390 
391 /* Symmetric of _ide_source_iter_forward_visible_word_end(). */
392 gboolean
_ide_source_iter_backward_visible_word_start(GtkTextIter * iter)393 _ide_source_iter_backward_visible_word_start (GtkTextIter *iter)
394 {
395 	GtkTextIter orig = *iter;
396 	GtkTextIter farthest = *iter;
397 	GtkTextIter prev_word_start = *iter;
398 	GtkTextIter word_end;
399 
400 	/* 'farthest' is the farthest position that this function can return. Example:
401 	 * "aaaa ----|"  ->  "aaaa |----"
402 	 */
403 	_ide_source_iter_backward_full_word_start (&farthest);
404 
405 	/* Go to the previous extra-natural word start. It can be farther than
406 	 * 'farthest':
407 	 * "aaaa ----|"  ->  "|aaaa ----"
408 	 *
409 	 * Or it can remain at the same place:
410 	 * "---- |aaaa"  ->  "---- |aaaa"
411 	 */
412 	_ide_source_iter_backward_extra_natural_word_start (&prev_word_start);
413 
414 	if (gtk_text_iter_compare (&prev_word_start, &farthest) < 0 ||
415 	    gtk_text_iter_equal (iter, &prev_word_start))
416 	{
417 		*iter = farthest;
418 		goto end;
419 	}
420 
421 	/* From 'prev_word_start', go to the next extra-natural word end.
422 	 *
423 	 * Example 1:
424 	 * iter:            "ab|cd"
425 	 * prev_word_start: "|abcd" -> the good one
426 	 * word_end:        "abcd|"
427 	 *
428 	 * Example 2:
429 	 * iter:            "()abcd |"
430 	 * prev_word_start: "()|abcd " -> the good one
431 	 * word_end:        "()abcd| "
432 	 *
433 	 * Example 3:
434 	 * iter:            "abcd()|"
435 	 * prev_word_start: "|abcd()"
436 	 * word_end:        "abcd|()" -> the good one, at the start of the word "()".
437 	 */
438 	word_end = prev_word_start;
439 	_ide_source_iter_forward_extra_natural_word_end (&word_end);
440 
441 	/* Example 1 */
442 	if (gtk_text_iter_compare (iter, &word_end) <= 0)
443 	{
444 		*iter = prev_word_start;
445 	}
446 
447 	/* Example 2 */
448 	else if (_ide_source_iter_ends_full_word (&word_end))
449 	{
450 		*iter = prev_word_start;
451 	}
452 
453 	/* Example 3 */
454 	else
455 	{
456 		*iter = word_end;
457 	}
458 
459 end:
460 	return !gtk_text_iter_equal (&orig, iter) && !gtk_text_iter_is_end (iter);
461 }
462 
463 /* Similar to gtk_text_iter_forward_visible_word_ends(). */
464 gboolean
_ide_source_iter_forward_visible_word_ends(GtkTextIter * iter,gint count)465 _ide_source_iter_forward_visible_word_ends (GtkTextIter *iter,
466 					    gint         count)
467 {
468 	GtkTextIter orig = *iter;
469 	gint i;
470 
471 	if (count < 0)
472 	{
473 		return _ide_source_iter_backward_visible_word_starts (iter, -count);
474 	}
475 
476 	for (i = 0; i < count; i++)
477 	{
478 		if (!_ide_source_iter_forward_visible_word_end (iter))
479 		{
480 			break;
481 		}
482 	}
483 
484 	return !gtk_text_iter_equal (&orig, iter) && !gtk_text_iter_is_end (iter);
485 }
486 
487 /* Similar to gtk_text_iter_backward_visible_word_starts(). */
488 gboolean
_ide_source_iter_backward_visible_word_starts(GtkTextIter * iter,gint count)489 _ide_source_iter_backward_visible_word_starts (GtkTextIter *iter,
490 					       gint         count)
491 {
492 	GtkTextIter orig = *iter;
493 	gint i;
494 
495 	if (count < 0)
496 	{
497 		return _ide_source_iter_forward_visible_word_ends (iter, -count);
498 	}
499 
500 	for (i = 0; i < count; i++)
501 	{
502 		if (!_ide_source_iter_backward_visible_word_start (iter))
503 		{
504 			break;
505 		}
506 	}
507 
508 	return !gtk_text_iter_equal (&orig, iter) && !gtk_text_iter_is_end (iter);
509 }
510 
511 gboolean
_ide_source_iter_starts_word(const GtkTextIter * iter)512 _ide_source_iter_starts_word (const GtkTextIter *iter)
513 {
514 	if (_ide_source_iter_starts_full_word (iter) ||
515 	    _ide_source_iter_starts_extra_natural_word (iter))
516 	{
517 		return TRUE;
518 	}
519 
520 	/* Example: "abcd|()", at the start of the word "()". */
521 	return (!_ide_source_iter_ends_full_word (iter) &&
522 		_ide_source_iter_ends_extra_natural_word (iter));
523 }
524 
525 gboolean
_ide_source_iter_ends_word(const GtkTextIter * iter)526 _ide_source_iter_ends_word (const GtkTextIter *iter)
527 {
528 	if (_ide_source_iter_ends_full_word (iter) ||
529 	    _ide_source_iter_ends_extra_natural_word (iter))
530 	{
531 		return TRUE;
532 	}
533 
534 	/* Example: "abcd()|efgh", at the end of the word "()". */
535 	return (!_ide_source_iter_starts_full_word (iter) &&
536 		_ide_source_iter_starts_extra_natural_word (iter));
537 }
538 
539 gboolean
_ide_source_iter_inside_word(const GtkTextIter * iter)540 _ide_source_iter_inside_word (const GtkTextIter *iter)
541 {
542 	GtkTextIter prev_word_start;
543 	GtkTextIter word_end;
544 
545 	if (_ide_source_iter_starts_word (iter))
546 	{
547 		return TRUE;
548 	}
549 
550 	prev_word_start = *iter;
551 	if (!_ide_source_iter_backward_visible_word_start (&prev_word_start))
552 	{
553 		return FALSE;
554 	}
555 
556 	word_end = prev_word_start;
557 	_ide_source_iter_forward_visible_word_end (&word_end);
558 
559 	return (gtk_text_iter_compare (&prev_word_start, iter) <= 0 &&
560 		gtk_text_iter_compare (iter, &word_end) < 0);
561 }
562 
563 /* Used for the GtkTextView::extend-selection signal. */
564 void
_ide_source_iter_extend_selection_word(const GtkTextIter * location,GtkTextIter * start,GtkTextIter * end)565 _ide_source_iter_extend_selection_word (const GtkTextIter *location,
566 					GtkTextIter       *start,
567 					GtkTextIter       *end)
568 {
569 	/* Exactly the same algorithm as in GTK+, but with our custom word
570 	 * boundaries.
571 	 */
572 	*start = *location;
573 	*end = *location;
574 
575 	if (_ide_source_iter_inside_word (start))
576 	{
577 		if (!_ide_source_iter_starts_word (start))
578 		{
579 			_ide_source_iter_backward_visible_word_start (start);
580 		}
581 
582 		if (!_ide_source_iter_ends_word (end))
583 		{
584 			_ide_source_iter_forward_visible_word_end (end);
585 		}
586 	}
587 	else
588 	{
589 		GtkTextIter tmp;
590 
591 		tmp = *start;
592 		if (_ide_source_iter_backward_visible_word_start (&tmp))
593 		{
594 			_ide_source_iter_forward_visible_word_end (&tmp);
595 		}
596 
597 		if (gtk_text_iter_get_line (&tmp) == gtk_text_iter_get_line (start))
598 		{
599 			*start = tmp;
600 		}
601 		else
602 		{
603 			gtk_text_iter_set_line_offset (start, 0);
604 		}
605 
606 		tmp = *end;
607 		if (!_ide_source_iter_forward_visible_word_end (&tmp))
608 		{
609 			gtk_text_iter_forward_to_end (&tmp);
610 		}
611 
612 		if (_ide_source_iter_ends_word (&tmp))
613 		{
614 			_ide_source_iter_backward_visible_word_start (&tmp);
615 		}
616 
617 		if (gtk_text_iter_get_line (&tmp) == gtk_text_iter_get_line (end))
618 		{
619 			*end = tmp;
620 		}
621 		else
622 		{
623 			gtk_text_iter_forward_to_line_end (end);
624 		}
625 	}
626 }
627