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