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