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