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