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