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