Cleanup: style
[blender.git] / source / blender / editors / space_text / text_autocomplete.c
1 /*
2  * ***** BEGIN GPL LICENSE BLOCK *****
3  *
4  * This program is free software; you can redistribute it and/or
5  * modify it under the terms of the GNU General Public License
6  * as published by the Free Software Foundation; either version 2
7  * of the License, or (at your option) any later version.
8  *
9  * This program is distributed in the hope that it will be useful,
10  * but WITHOUT ANY WARRANTY; without even the implied warranty of
11  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12  * GNU General Public License for more details.
13  *
14  * You should have received a copy of the GNU General Public License
15  * along with this program; if not, write to the Free Software Foundation,
16  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17  *
18  * ***** END GPL LICENSE BLOCK *****
19  */
20
21 /** \file blender/editors/space_text/text_autocomplete.c
22  *  \ingroup sptext
23  */
24
25 #include <ctype.h>
26 #include <string.h>
27
28 #include "MEM_guardedalloc.h"
29
30 #include "DNA_text_types.h"
31
32 #include "BLI_blenlib.h"
33 #include "BLI_ghash.h"
34
35 #include "BKE_context.h"
36 #include "BKE_text.h"
37 #include "BKE_screen.h"
38 #include "BKE_suggestions.h"
39
40 #include "WM_api.h"
41 #include "WM_types.h"
42
43 #include "ED_screen.h"
44 #include "UI_interface.h"
45
46 #include "text_format.h"
47 #include "text_intern.h"  /* own include */
48
49
50 /* -------------------------------------------------------------------- */
51 /* Public API */
52
53 int text_do_suggest_select(SpaceText *st, ARegion *ar)
54 {
55         SuggItem *item, *first, *last /* , *sel */ /* UNUSED */;
56         TextLine *tmp;
57         int l, x, y, w, h, i;
58         int tgti, *top;
59         int mval[2] = {0, 0};
60
61         if (!st || !st->text) return 0;
62         if (!texttool_text_is_active(st->text)) return 0;
63
64         first = texttool_suggest_first();
65         last = texttool_suggest_last();
66         /* sel = texttool_suggest_selected(); */ /* UNUSED */
67         top = texttool_suggest_top();
68
69         if (!last || !first)
70                 return 0;
71
72         /* Count the visible lines to the cursor */
73         for (tmp = st->text->curl, l = -st->top; tmp; tmp = tmp->prev, l++) ;
74         if (l < 0) return 0;
75
76         text_update_character_width(st);
77
78         if (st->showlinenrs) {
79                 x = st->cwidth * (st->text->curc - st->left) + TXT_OFFSET + TEXTXLOC - 4;
80         }
81         else {
82                 x = st->cwidth * (st->text->curc - st->left) + TXT_OFFSET - 4;
83         }
84         y = ar->winy - st->lheight_dpi * l - 2;
85
86         w = SUGG_LIST_WIDTH * st->cwidth + U.widget_unit;
87         h = SUGG_LIST_SIZE * st->lheight_dpi + 0.4f * U.widget_unit;
88
89         // XXX getmouseco_areawin(mval);
90
91         if (mval[0] < x || x + w < mval[0] || mval[1] < y - h || y < mval[1])
92                 return 0;
93
94         /* Work out which of the items is at the top of the visible list */
95         for (i = 0, item = first; i < *top && item->next; i++, item = item->next) ;
96
97         /* Work out the target item index in the visible list */
98         tgti = (y - mval[1] - 4) / st->lheight_dpi;
99         if (tgti < 0 || tgti > SUGG_LIST_SIZE)
100                 return 1;
101
102         for (i = tgti; i > 0 && item->next; i--, item = item->next) ;
103         if (item)
104                 texttool_suggest_select(item);
105         return 1;
106 }
107
108 void text_pop_suggest_list(void)
109 {
110         SuggItem *item, *sel;
111         int *top, i;
112
113         item = texttool_suggest_first();
114         sel = texttool_suggest_selected();
115         top = texttool_suggest_top();
116
117         i = 0;
118         while (item && item != sel) {
119                 item = item->next;
120                 i++;
121         }
122         if (i > *top + SUGG_LIST_SIZE - 1)
123                 *top = i - SUGG_LIST_SIZE + 1;
124         else if (i < *top)
125                 *top = i;
126 }
127
128 /* -------------------------------------------------------------------- */
129 /* Private API */
130
131 static void text_autocomplete_free(bContext *C, wmOperator *op);
132
133 static GHash *text_autocomplete_build(Text *text)
134 {
135         GHash *gh;
136         int seek_len = 0;
137         const char *seek;
138         texttool_text_clear();
139
140         texttool_text_set_active(text);
141
142         /* first get the word we're at */
143         {
144                 const int i = text_find_identifier_start(text->curl->line, text->curc);
145                 seek_len = text->curc - i;
146                 seek = text->curl->line + i;
147
148                 // BLI_strncpy(seek, seek_ptr, seek_len);
149         }
150
151         /* now walk over entire doc and suggest words */
152         {
153                 TextLine *linep;
154
155                 gh = BLI_ghash_str_new(__func__);
156
157                 for (linep = text->lines.first; linep; linep = linep->next) {
158                         size_t i_start = 0;
159                         size_t i_end = 0;
160                         size_t i_pos = 0;
161
162                         while (i_start < linep->len) {
163                                 /* seek identifier beginning */
164                                 i_pos = i_start;
165                                 while ((i_start < linep->len) &&
166                                        (!text_check_identifier_nodigit_unicode(BLI_str_utf8_as_unicode_and_size_safe(&linep->line[i_start], &i_pos))))
167                                 {
168                                         i_start = i_pos;
169                                 }
170                                 i_pos = i_end = i_start;
171                                 while ((i_end < linep->len) &&
172                                        (text_check_identifier_unicode(BLI_str_utf8_as_unicode_and_size_safe(&linep->line[i_end], &i_pos))))
173                                 {
174                                         i_end = i_pos;
175                                 }
176
177                                 if ((i_start != i_end) &&
178                                     /* check we're at the beginning of a line or that the previous char is not an identifier
179                                      * this prevents digits from being added */
180                                     ((i_start < 1) || !text_check_identifier_unicode(BLI_str_utf8_as_unicode(&linep->line[i_start - 1]))))
181                                 {
182                                         char *str_sub = &linep->line[i_start];
183                                         const int choice_len = i_end - i_start;
184
185                                         if ((choice_len > seek_len) &&
186                                             (seek_len == 0 || STREQLEN(seek, str_sub, seek_len)) &&
187                                             (seek != str_sub))
188                                         {
189                                                 // printf("Adding: %s\n", s);
190                                                 char str_sub_last = str_sub[choice_len];
191                                                 str_sub[choice_len] = '\0';
192                                                 if (!BLI_ghash_lookup(gh, str_sub)) {
193                                                         char *str_dup = BLI_strdupn(str_sub, choice_len);
194                                                         BLI_ghash_insert(gh, str_dup, str_dup);  /* A 'set' would make more sense here */
195                                                 }
196                                                 str_sub[choice_len] = str_sub_last;
197                                         }
198                                 }
199                                 if (i_end != i_start) {
200                                         i_start = i_end;
201                                 }
202                                 else {
203                                         /* highly unlikely, but prevent eternal loop */
204                                         i_start++;
205                                 }
206                         }
207                 }
208
209                 {
210                         GHashIterator gh_iter;
211
212                         /* get the formatter for highlighting */
213                         TextFormatType *tft;
214                         tft = ED_text_format_get(text);
215
216                         GHASH_ITER (gh_iter, gh) {
217                                 const char *s = BLI_ghashIterator_getValue(&gh_iter);
218                                 texttool_suggest_add(s, tft->format_identifier(s));
219                         }
220                 }
221         }
222
223         texttool_suggest_prefix(seek, seek_len);
224
225         return gh;
226 }
227
228 /* -- */
229
230 static void get_suggest_prefix(Text *text, int offset)
231 {
232         int i, len;
233         const char *line;
234
235         if (!text) return;
236         if (!texttool_text_is_active(text)) return;
237
238         line = text->curl->line;
239         i = text_find_identifier_start(line, text->curc + offset);
240         len = text->curc - i + offset;
241         texttool_suggest_prefix(line + i, len);
242 }
243
244 static void confirm_suggestion(Text *text)
245 {
246         SuggItem *sel;
247         int i, over = 0;
248         const char *line;
249
250         if (!text) return;
251         if (!texttool_text_is_active(text)) return;
252
253         sel = texttool_suggest_selected();
254         if (!sel) return;
255
256         line = text->curl->line;
257         i = text_find_identifier_start(line, text->curc /* - skipleft */);
258         over = text->curc - i;
259
260 //      for (i = 0; i < skipleft; i++)
261 //              txt_move_left(text, 0);
262         BLI_assert(memcmp(sel->name, &line[i], over) == 0);
263         txt_insert_buf(text, sel->name + over);
264
265 //      for (i = 0; i < skipleft; i++)
266 //              txt_move_right(text, 0);
267
268         texttool_text_clear();
269 }
270
271 /* -- */
272
273
274 static int text_autocomplete_invoke(bContext *C, wmOperator *op, const wmEvent *UNUSED(event))
275 {
276         SpaceText *st = CTX_wm_space_text(C);
277         Text *text = CTX_data_edit_text(C);
278
279         st->doplugins = true;
280         op->customdata = text_autocomplete_build(text);
281
282         if (texttool_suggest_first()) {
283
284                 ED_area_tag_redraw(CTX_wm_area(C));
285
286                 if (texttool_suggest_first() == texttool_suggest_last()) {
287                         confirm_suggestion(st->text);
288                         text_update_line_edited(st->text->curl);
289                         text_autocomplete_free(C, op);
290                         return OPERATOR_FINISHED;
291                 }
292                 else {
293                         WM_event_add_modal_handler(C, op);
294                         return OPERATOR_RUNNING_MODAL;
295                 }
296         }
297         else {
298                 text_autocomplete_free(C, op);
299                 return OPERATOR_CANCELLED;
300         }
301 }
302
303 static int doc_scroll = 0;
304
305 static int text_autocomplete_modal(bContext *C, wmOperator *op, const wmEvent *event)
306 {
307         SpaceText *st = CTX_wm_space_text(C);
308         ScrArea *sa = CTX_wm_area(C);
309         ARegion *ar = BKE_area_find_region_type(sa, RGN_TYPE_WINDOW);
310
311         int draw = 0, tools = 0, swallow = 0, scroll = 1;
312         Text *text = CTX_data_edit_text(C);
313         int retval = OPERATOR_RUNNING_MODAL;
314
315         (void)text;
316
317         if (st->doplugins && texttool_text_is_active(st->text)) {
318                 if (texttool_suggest_first()) tools |= TOOL_SUGG_LIST;
319                 if (texttool_docs_get()) tools |= TOOL_DOCUMENT;
320         }
321
322         switch (event->type) {
323                 case LEFTMOUSE:
324                         if (event->val == KM_PRESS) {
325                                 if (text_do_suggest_select(st, ar))
326                                         swallow = 1;
327                                 else {
328                                         if (tools & TOOL_SUGG_LIST) {
329                                                 texttool_suggest_clear();
330                                         }
331                                         if (tools & TOOL_DOCUMENT) {
332                                                 texttool_docs_clear();
333                                                 doc_scroll = 0;
334                                         }
335                                         retval = OPERATOR_FINISHED;
336                                 }
337                                 draw = 1;
338                         }
339                         break;
340                 case MIDDLEMOUSE:
341                         if (event->val == KM_PRESS) {
342                                 if (text_do_suggest_select(st, ar)) {
343                                         confirm_suggestion(st->text);
344                                         text_update_line_edited(st->text->curl);
345                                         swallow = 1;
346                                 }
347                                 else {
348                                         if (tools & TOOL_SUGG_LIST) {
349                                                 texttool_suggest_clear();
350                                         }
351                                         if (tools & TOOL_DOCUMENT) {
352                                                 texttool_docs_clear();
353                                                 doc_scroll = 0;
354                                         }
355                                         retval = OPERATOR_FINISHED;
356                                 }
357                                 draw = 1;
358                         }
359                         break;
360                 case ESCKEY:
361                         if (event->val == KM_PRESS) {
362                                 draw = swallow = 1;
363                                 if (tools & TOOL_SUGG_LIST) {
364                                         texttool_suggest_clear();
365                                 }
366                                 else if (tools & TOOL_DOCUMENT) {
367                                         texttool_docs_clear();
368                                         doc_scroll = 0;
369                                 }
370                                 else draw = swallow = 0;
371                                 retval = OPERATOR_CANCELLED;
372                         }
373                         break;
374                 case RETKEY:
375                 case PADENTER:
376                         if (event->val == KM_PRESS) {
377                                 if (tools & TOOL_SUGG_LIST) {
378                                         confirm_suggestion(st->text);
379                                         text_update_line_edited(st->text->curl);
380                                         swallow = 1;
381                                         draw = 1;
382                                 }
383                                 if (tools & TOOL_DOCUMENT) {
384                                         texttool_docs_clear();
385                                         doc_scroll = 0;
386                                         draw = 1;
387                                 }
388                                 retval = OPERATOR_FINISHED;
389                         }
390                         break;
391                 case LEFTARROWKEY:
392                 case BACKSPACEKEY:
393                         if (event->val == KM_PRESS) {
394                                 if (tools & TOOL_SUGG_LIST) {
395                                         if (event->ctrl) {
396                                                 texttool_suggest_clear();
397                                                 retval = OPERATOR_CANCELLED;
398                                         }
399                                         else {
400                                                 /* Work out which char we are about to delete/pass */
401                                                 if (st->text->curl && st->text->curc > 0) {
402                                                         char ch = st->text->curl->line[st->text->curc - 1];
403                                                         if ((ch == '_' || !ispunct(ch)) && !text_check_whitespace(ch)) {
404                                                                 get_suggest_prefix(st->text, -1);
405                                                                 text_pop_suggest_list();
406                                                         }
407                                                         else {
408                                                                 texttool_suggest_clear();
409                                                                 retval = OPERATOR_CANCELLED;
410                                                         }
411                                                 }
412                                                 else {
413                                                         texttool_suggest_clear();
414                                                         retval = OPERATOR_CANCELLED;
415                                                 }
416                                         }
417                                 }
418                                 if (tools & TOOL_DOCUMENT) {
419                                         texttool_docs_clear();
420                                         doc_scroll = 0;
421                                 }
422                         }
423                         break;
424                 case RIGHTARROWKEY:
425                         if (event->val == KM_PRESS) {
426                                 if (tools & TOOL_SUGG_LIST) {
427                                         if (event->ctrl) {
428                                                 texttool_suggest_clear();
429                                                 retval = OPERATOR_CANCELLED;
430                                         }
431                                         else {
432                                                 /* Work out which char we are about to pass */
433                                                 if (st->text->curl && st->text->curc < st->text->curl->len) {
434                                                         char ch = st->text->curl->line[st->text->curc + 1];
435                                                         if ((ch == '_' || !ispunct(ch)) && !text_check_whitespace(ch)) {
436                                                                 get_suggest_prefix(st->text, 1);
437                                                                 text_pop_suggest_list();
438                                                         }
439                                                         else {
440                                                                 texttool_suggest_clear();
441                                                                 retval = OPERATOR_CANCELLED;
442                                                         }
443                                                 }
444                                                 else {
445                                                         texttool_suggest_clear();
446                                                         retval = OPERATOR_CANCELLED;
447                                                 }
448                                         }
449                                 }
450                                 if (tools & TOOL_DOCUMENT) {
451                                         texttool_docs_clear();
452                                         doc_scroll = 0;
453                                 }
454                         }
455                         break;
456                 case PAGEDOWNKEY:
457                         scroll = SUGG_LIST_SIZE - 1;
458                         ATTR_FALLTHROUGH;
459                 case WHEELDOWNMOUSE:
460                 case DOWNARROWKEY:
461                         if (event->val == KM_PRESS) {
462                                 if (tools & TOOL_DOCUMENT) {
463                                         doc_scroll++;
464                                         swallow = 1;
465                                         draw = 1;
466                                 }
467                                 else if (tools & TOOL_SUGG_LIST) {
468                                         SuggItem *sel = texttool_suggest_selected();
469                                         if (!sel) {
470                                                 texttool_suggest_select(texttool_suggest_first());
471                                         }
472                                         else {
473                                                 while (sel && scroll--) {
474                                                         if (sel != texttool_suggest_last() && sel->next) {
475                                                                 texttool_suggest_select(sel->next);
476                                                                 sel = sel->next;
477                                                         }
478                                                         else {
479                                                                 texttool_suggest_select(texttool_suggest_first());
480                                                                 sel = texttool_suggest_first();
481                                                         }
482                                                 }
483                                         }
484                                         text_pop_suggest_list();
485                                         swallow = 1;
486                                         draw = 1;
487                                 }
488                         }
489                         break;
490                 case PAGEUPKEY:
491                         scroll = SUGG_LIST_SIZE - 1;
492                         ATTR_FALLTHROUGH;
493                 case WHEELUPMOUSE:
494                 case UPARROWKEY:
495                         if (event->val == KM_PRESS) {
496                                 if (tools & TOOL_DOCUMENT) {
497                                         if (doc_scroll > 0) doc_scroll--;
498                                         swallow = 1;
499                                         draw = 1;
500                                 }
501                                 else if (tools & TOOL_SUGG_LIST) {
502                                         SuggItem *sel = texttool_suggest_selected();
503                                         while (sel && scroll--) {
504                                                 if (sel != texttool_suggest_first() && sel->prev) {
505                                                         texttool_suggest_select(sel->prev);
506                                                         sel = sel->prev;
507                                                 }
508                                                 else {
509                                                         texttool_suggest_select(texttool_suggest_last());
510                                                         sel = texttool_suggest_last();
511                                                 }
512                                         }
513                                         text_pop_suggest_list();
514                                         swallow = 1;
515                                         draw = 1;
516                                 }
517                         }
518                         break;
519                 case RIGHTSHIFTKEY:
520                 case LEFTSHIFTKEY:
521                         break;
522 #if 0
523                 default:
524                         if (tools & TOOL_SUGG_LIST) {
525                                 texttool_suggest_clear();
526                                 draw = 1;
527                         }
528                         if (tools & TOOL_DOCUMENT) {
529                                 texttool_docs_clear();
530                                 doc_scroll = 0;
531                                 draw = 1;
532                         }
533 #endif
534         }
535
536         if (draw) {
537                 ED_area_tag_redraw(sa);
538         }
539
540 //      if (swallow) {
541 //              retval = OPERATOR_RUNNING_MODAL;
542 //      }
543
544         if (texttool_suggest_first()) {
545                 if (retval != OPERATOR_RUNNING_MODAL) {
546                         text_autocomplete_free(C, op);
547                 }
548                 return retval;
549         }
550         else {
551                 text_autocomplete_free(C, op);
552                 return OPERATOR_FINISHED;
553         }
554 }
555
556 static void text_autocomplete_free(bContext *C, wmOperator *op)
557 {
558         GHash *gh = op->customdata;
559         if (gh) {
560                 BLI_ghash_free(gh, NULL, MEM_freeN);
561                 op->customdata = NULL;
562         }
563
564         /* other stuff */
565         {
566                 SpaceText *st = CTX_wm_space_text(C);
567                 st->doplugins = false;
568                 texttool_text_clear();
569         }
570 }
571
572 static void text_autocomplete_cancel(bContext *C, wmOperator *op)
573 {
574         text_autocomplete_free(C, op);
575 }
576
577 void TEXT_OT_autocomplete(wmOperatorType *ot)
578 {
579         /* identifiers */
580         ot->name = "Text Auto Complete";
581         ot->description = "Show a list of used text in the open document";
582         ot->idname = "TEXT_OT_autocomplete";
583
584         /* api callbacks */
585         ot->invoke = text_autocomplete_invoke;
586         ot->cancel = text_autocomplete_cancel;
587         ot->modal = text_autocomplete_modal;
588         ot->poll = text_space_edit_poll;
589
590         /* flags */
591         ot->flag = OPTYPE_BLOCKING;
592 }