1// This file is part of SmallBASIC
2//
3// Copyright(C) 2001-2019 Chris Warren-Smith.
4//
5// This program is distributed under the terms of the GPL v2.0 or later
6// Download the GNU Public License (GPL) from www.gnu.org
7//
8
9#include <stdlib.h>
10#include <string.h>
11
12#include "ui/textedit.h"
13#include "ui/inputs.h"
14#include "ui/utils.h"
15#include "ui/strlib.h"
16#include "ui/kwp.h"
17
18void safe_memmove(void *dest, const void *src, size_t n) {
19 if (n > 0 && dest != nullptr && src != nullptr) {
20 memmove(dest, src, n);
21 }
22}
23
24#define STB_TEXTEDIT_IS_SPACE(ch) IS_WHITE(ch)
25#define STB_TEXTEDIT_IS_PUNCT(ch) (ch != '_' && ch != '$' && ispunct(ch))
26#define IS_VAR_CHAR(ch) (ch == '_' || ch == '$' || isalpha(ch) || isdigit(ch))
27#define STB_TEXTEDIT_memmove safe_memmove
28#define STB_TEXTEDIT_IMPLEMENTATION
29
30int is_word_border(EditBuffer *_str, int _idx) {
31 return _idx > 0 ? ((STB_TEXTEDIT_IS_SPACE(STB_TEXTEDIT_GETCHAR(_str,_idx-1)) ||
32 STB_TEXTEDIT_IS_PUNCT(STB_TEXTEDIT_GETCHAR(_str,_idx-1))) &&
33 !STB_TEXTEDIT_IS_SPACE(STB_TEXTEDIT_GETCHAR(_str, _idx))) : 1;
34}
35
36int textedit_move_to_word_previous(EditBuffer *str, int c) {
37 --c; // always move at least one character
38 while (c >= 0 && !is_word_border(str, c)) {
39 --c;
40 }
41 if (c < 0) {
42 c = 0;
43 }
44 return c;
45}
46
47int textedit_move_to_word_next(EditBuffer *str, int c) {
48 const int len = str->_len;
49 ++c; // always move at least one character
50 while (c < len && !is_word_border(str, c)) {
51 ++c;
52 }
53 if (c > len) {
54 c = len;
55 }
56 return c;
57}
58
59#define STB_TEXTEDIT_MOVEWORDLEFT textedit_move_to_word_previous
60#define STB_TEXTEDIT_MOVEWORDRIGHT textedit_move_to_word_next
61
62#pragma GCC diagnostic ignored "-Wunused-function"
63#include "lib/stb/stb_textedit.h"
64#pragma GCC diagnostic pop
65
66#define GROW_SIZE 128
67#define LINE_BUFFER_SIZE 200
68#define INDENT_LEVEL 2
69#define HELP_WIDTH 22
70#define TWISTY1_OPEN "> "
71#define TWISTY1_CLOSE "< "
72#define TWISTY2_OPEN " > "
73#define TWISTY2_CLOSE " < "
74#define TWISTY1_LEN 2
75#define TWISTY2_LEN 4
76#define HELP_BG 0x20242a
77#define HELP_FG 0x73c990
78#define DOUBLE_CLICK_MS 200
79#define SIDE_BAR_WIDTH 30
80
81#if defined(_Win32)
82#include <shlwapi.h>
83#define strcasestr StrStrI
84#endif
85
86extern "C" uint32_t dev_get_millisecond_count();
87
88unsigned g_themeId = 0;
89int g_lineMarker[MAX_MARKERS] = {
90 -1, -1, -1, -1, -1, -1, -1, -1, -1, -1
91};
92
93const char *themeName() {
94 switch (g_themeId) {
95 case 0: return "Dark";
96 case 1: return "Light";
97 case 2: return "Shian";
98 case 3: return "ATOM 1";
99 case 4: return "ATOM 2";
100 case 5: return "R157";
101 default: return "";
102 }
103}
104
105// see: http://ethanschoonover.com/solarized#features
106#define sol_base03 0x002b36
107#define sol_base02 0x073642
108#define sol_base01 0x586e75
109#define sol_base00 0x657b83
110#define sol_base0 0x839496
111#define sol_base1 0x93a1a1
112#define sol_base2 0xeee8d5
113#define sol_base3 0xfdf6e3
114#define sol_yellow 0xb58900
115#define sol_orange 0xcb4b16
116#define sol_red 0xdc322f
117#define sol_magenta 0xd33682
118#define sol_violet 0x6c71c4
119#define sol_blue 0x268bd2
120#define sol_cyan 0x2aa198
121#define sol_green 0x859900
122
123// 0 - color
124// 1 - selection_color
125// 2 - number_color
126// 3 - number_selection_color
127// 4 - cursor_color
128// 5 - syntax_comments
129// 6 - background
130// 7 - selection_background
131// 8 - number_selection_background
132// 9 - cursor_background
133// 10 - match_background
134// 11 - row_cursor
135// 12 - syntax_text
136// 13 - syntax_command
137// 14 - syntax_statement
138// 15 - syntax_digit
139// 16 - row_marker
140
141const int solarized_dark[] = {
142 sol_base0, sol_base02, sol_base01, sol_base02, 0xa7aebc, sol_base01,
143 sol_base03, sol_base1, sol_base0, 0x3875ed, 0x373b88, sol_base02,
144 sol_green, sol_yellow, sol_blue, sol_cyan, 0x0083f8
145};
146
147const int solarized_light[] = {
148 sol_base00, sol_base02, sol_base1, sol_base03, 0xa7aebc, sol_base1,
149 sol_base3, sol_base1, sol_base0, 0x3875ed, 0x373b88, sol_base2,
150 sol_green, sol_violet, sol_yellow, sol_blue, 0x0083f8
151};
152
153const int shian[] = {
154 0xcccccc, 0x000077, 0x333333, 0x333333, 0x0000aa, 0x008888,
155 0x010101, 0xeeeeee, 0x010101, 0xffff00, 0x00ff00, 0x010101,
156 0x00ffff, 0xff00ff, 0xffffff, 0x00ffff, 0x00aaff
157};
158
159const int atom1[] = {
160 0xc8cedb, 0xa7aebc, 0x484f5f, 0xa7aebc, 0xa7aebc, 0x00bb00,
161 0x272b33, 0x3d4350, 0x2b3039, 0x3875ed, 0x373b88, 0x2b313a,
162 0x0083f8, 0xff9d00, 0x31ccac, 0xc679dd, 0x0083f8
163};
164
165const int atom2[] = {
166 0xc8cedb, 0xd7decc, 0x484f5f, 0xa7aebc, 0xa7aebc, 0x00bb00,
167 0x001b33, 0x0088ff, 0x000d1a, 0x0051b1, 0x373b88, 0x022444,
168 0x0083f8, 0xff9d00, 0x31ccac, 0xc679dd, 0x0083f8
169};
170
171const int r157[] = {
172 0x80dfff, 0xa7aebc, 0xffffff, 0xa7aebc, 0xa7aebc, 0xd0d6e1,
173 0x2e3436, 0x888a85, 0x000000, 0x4d483b, 0x000000, 0x576375,
174 0xffffff, 0xffc466, 0xffcce0, 0xffff66, 0x0083f8
175};
176
177int g_user_theme[] = {
178 0xc8cedb, 0xa7aebc, 0x484f5f, 0xa7aebc, 0xa7aebc, 0x00bb00,
179 0x2e3436, 0x888a85, 0x000000, 0x4d483b, 0x000000, 0x2b313a,
180 0x0083f8, 0xff9d00, 0x31ccac, 0xc679dd, 0x0083f8
181};
182
183const int* themes[] = {
184 solarized_dark, solarized_light, shian, atom1, atom2, r157, g_user_theme
185};
186
187const char *helpText =
188 "C-a select-all\n"
189 "C-b back, exit\n"
190 "C-k delete line\n"
191 "C-d delete char\n"
192 "C-s save\n"
193 "C-x cut\n"
194 "C-c copy\n"
195 "C-v paste\n"
196 "C-z undo\n"
197 "C-y redo\n"
198 "C-f find, find-next\n"
199 "C-n find, replace\n"
200 "C-t toggle marker\n"
201 "C-g goto marker\n"
202 "C-l outline\n"
203 "C-o show output\n"
204 "C-SPC auto-complete\n"
205 "C-home top\n"
206 "C-end bottom\n"
207 "C-5,6,7 macro\n"
208 "A-c change case\n"
209 "A-d kill word\n"
210 "A-g goto line\n"
211 "A-n trim line-endings\n"
212 "A-t select theme\n"
213 "A-w select word\n"
214 "A-. return mode\n"
215 "A-<n> recent file\n"
216 "A-= count chars\n"
217 "SHIFT-<arrow> select\n"
218 "TAB indent line\n"
219 "F1,A-h keyword help\n"
220 "F2 online help\n"
221 "F3,F4 export\n"
222 "F5,F6,F7 debug\n"
223 "F8 live edit\n"
224 "F9, C-r run\n"
225 "F10 set command$\n"
226 "F11 full screen\n";
227
228inline bool match(const char *str, const char *pattern , int len) {
229 int i, j;
230 for (i = 0, j = 0; i < len; i++, j += 2) {
231 if (str[i] != pattern[j] && str[i] != pattern[j + 1]) {
232 break;
233 }
234 }
235 return i == len;
236}
237
238inline bool is_comment(const char *str, int offs) {
239 return (str[offs] == '\'' || (str[offs] == '#' && !isdigit(str[offs + 1]))
240 || match(str + offs, "RrEeMm ", 3));
241}
242
243int compareIntegers(const void *p1, const void *p2) {
244 int i1 = *((int *)p1);
245 int i2 = *((int *)p2);
246 return i1 < i2 ? -1 : i1 == i2 ? 0 : 1;
247}
248
249const char *find_str(bool allUpper, const char *haystack, const char *needle) {
250 return allUpper ? strstr(haystack, needle) : strcasestr(haystack, needle);
251}
252
253int shade(int c, float weight) {
254 uint8_t r = ((uint8_t)(c >> 16));
255 uint8_t g = ((uint8_t)(c >> 8));
256 uint8_t b = ((uint8_t)(c));
257 r = (r * weight);
258 g = (g * weight);
259 b = (b * weight);
260 r = r < 255 ? r : 255;
261 g = g < 255 ? g : 255;
262 b = b < 255 ? b : 255;
263 return (r << 16) + (g << 8) + (b);
264}
265
266//
267// EditTheme
268//
269EditTheme::EditTheme() {
270 if (g_themeId >= (sizeof(themes) / sizeof(themes[0]))) {
271 g_themeId = 0;
272 }
273 selectTheme(themes[g_themeId]);
274}
275
276EditTheme::EditTheme(int fg, int bg) :
277 _color(fg),
278 _background(bg),
279 _selection_color(bg),
280 _selection_background(fg),
281 _number_color(fg),
282 _number_selection_color(fg),
283 _number_selection_background(bg),
284 _cursor_color(bg),
285 _cursor_background(fg),
286 _match_background(fg),
287 _row_cursor(bg),
288 _syntax_comments(bg),
289 _syntax_text(fg),
290 _syntax_command(fg),
291 _syntax_statement(fg),
292 _syntax_digit(fg),
293 _row_marker(fg) {
294}
295
296void EditTheme::setId(const unsigned themeId) {
297 if (themeId >= (sizeof(themes) / sizeof(themes[0]))) {
298 selectTheme(themes[0]);
299 } else {
300 selectTheme(themes[themeId]);
301 }
302}
303
304void EditTheme::selectTheme(const int theme[]) {
305 _color = theme[0];
306 _selection_color = theme[1];
307 _number_color = theme[2];
308 _number_selection_color = theme[3];
309 _cursor_color = theme[4];
310 _syntax_comments = theme[5];
311 _background = theme[6];
312 _selection_background = theme[7];
313 _number_selection_background = theme[8];
314 _cursor_background = theme[9];
315 _match_background = theme[10];
316 _row_cursor = theme[11];
317 _syntax_text = theme[12];
318 _syntax_command = theme[13];
319 _syntax_statement = theme[14];
320 _syntax_digit = theme[15];
321 _row_marker = theme[16];
322}
323
324void EditTheme::contrast(EditTheme *other) {
325 int fg = shade(other->_color, .65);
326 int bg = shade(other->_background, .65);
327 _color = fg;
328 _background = bg;
329 _selection_color = bg;
330 _selection_background = shade(bg, .65);
331 _number_color = fg;
332 _number_selection_color = fg;
333 _number_selection_background = bg;
334 _cursor_color = bg;
335 _cursor_background = fg;
336 _match_background = fg;
337 _row_cursor = bg;
338 _syntax_comments = bg;
339 _syntax_text = fg;
340 _syntax_command = fg;
341 _syntax_statement = fg;
342 _syntax_digit = fg;
343 _row_marker = fg;
344}
345
346//
347// EditBuffer
348//
349EditBuffer::EditBuffer(TextEditInput *in, const char *text) :
350 _buffer(nullptr),
351 _len(0),
352 _size(0),
353 _lines(-1),
354 _in(in) {
355 if (text != nullptr && text[0]) {
356 _len = strlen(text);
357 _size = _len + 1;
358 _buffer = (char *)malloc(_size);
359 memcpy(_buffer, text, _len);
360 _buffer[_len] = '\0';
361 }
362}
363
364EditBuffer::~EditBuffer() {
365 clear();
366}
367
368void EditBuffer::clear() {
369 free(_buffer);
370 _buffer = nullptr;
371 _len = _size = 0;
372 _lines = -1;
373}
374
375int EditBuffer::countNewlines(const char *text, int num) {
376 int result = 0;
377 for (int i = 0; i < num; i++) {
378 if (text[i] == '\n') {
379 result++;
380 }
381 }
382 return result;
383}
384
385int EditBuffer::deleteChars(int pos, int num) {
386 if (num > 1) {
387 _lines -= countNewlines(_buffer + pos, num);
388 } else if (_buffer[pos] == '\n') {
389 _lines--;
390 }
391
392 if (_len - (pos + num) > 0) {
393 memmove(&_buffer[pos], &_buffer[pos + num], _len - (pos + num));
394 }
395 // otherwise no more characters to pull back over the hole
396 _len -= num;
397 if (_len < 0) {
398 _len = 0;
399 }
400 _buffer[_len] = '\0';
401 _in->setDirty(true);
402 return 1;
403}
404
405char EditBuffer::getChar(int pos) {
406 char result;
407 if (_buffer != nullptr && pos >= 0 && pos < _len) {
408 result = _buffer[pos];
409 } else {
410 result = '\0';
411 }
412 return result;
413}
414
415int EditBuffer::insertChars(int pos, const char *text, int num) {
416 if (num == 1 && *text < 0) {
417 return 0;
418 }
419 int required = _len + num + 1;
420 if (required >= _size) {
421 _size += (required + GROW_SIZE);
422 _buffer = (char *)realloc(_buffer, _size);
423 }
424 if (_len - pos > 0) {
425 memmove(&_buffer[pos + num], &_buffer[pos], _len - pos);
426 }
427 memcpy(&_buffer[pos], text, num);
428 _len += num;
429 _buffer[_len] = '\0';
430 _in->setDirty(true);
431 if (num > 1) {
432 _lines += countNewlines(text, num);
433 } else if (text[0] == '\n') {
434 _lines++;
435 }
436 return 1;
437}
438
439int EditBuffer::lineCount() {
440 if (_lines < 0) {
441 _lines = 1 + countNewlines(_buffer, _len);
442 }
443 return _lines;
444}
445
446char *EditBuffer::textRange(int start, int end) {
447 char *result;
448 int len;
449 if (start < 0 || start > _len || end <= start) {
450 len = 0;
451 result = (char*)malloc(len + 1);
452 } else {
453 if (end > _len) {
454 end = _len;
455 }
456 len = end - start;
457 result = (char*)malloc(len + 1);
458 memcpy(result, &_buffer[start], len);
459 }
460 result[len] = '\0';
461 return result;
462}
463
464void EditBuffer::removeTrailingSpaces(STB_TexteditState *state) {
465 int lineEnd = _len - 1;
466 int lastChar = lineEnd;
467 bool atEnd = true;
468
469 for (int i = _len - 1; i >= 0; i--) {
470 if (_buffer[i] == '\n' || i == 0) {
471 // boundary - set new lineEnd
472 if (atEnd && lastChar < lineEnd) {
473 stb_textedit_delete(this, state, lastChar + 1, lineEnd - lastChar);
474 }
475 lineEnd = lastChar = i - 1;
476 atEnd = true;
477 } else if (atEnd) {
478 if (_buffer[i] == '\r' ||
479 _buffer[i] == '\t' ||
480 _buffer[i] == ' ') {
481 // whitespace
482 lastChar--;
483 } else {
484 // no more whitespace
485 if (lastChar < lineEnd) {
486 stb_textedit_delete(this, state, lastChar + 1, lineEnd - lastChar);
487 }
488 atEnd = false;
489 }
490 }
491 }
492}
493
494//
495// TextEditInput
496//
497TextEditInput::TextEditInput(const char *text, int chW, int chH,
498 int x, int y, int w, int h) :
499 FormEditInput(x, y, w, h),
500 _buf(this, text),
501 _theme(nullptr),
502 _charWidth(chW),
503 _charHeight(chH),
504 _marginWidth(0),
505 _scroll(0),
506 _cursorCol(0),
507 _cursorRow(0),
508 _cursorLine(0),
509 _indentLevel(INDENT_LEVEL),
510 _matchingBrace(-1),
511 _ptY(-1),
512 _pressTick(0),
513 _xmargin(0),
514 _ymargin(0),
515 _bottom(false),
516 _dirty(false) {
517 stb_textedit_initialize_state(&_state, false);
518 _resizable = true;
519}
520
521TextEditInput::~TextEditInput() {
522 delete _theme;
523 _theme = nullptr;
524}
525
526void TextEditInput::completeWord(const char *word) {
527 if (_state.select_start == _state.select_end) {
528 int start = wordStart();
529 int end = _state.cursor;
530 int len = end - start;
531 int insertLen = strlen(word) - len;
532 int index = end == 0 ? 0 : end - 1;
533 bool lastUpper = isupper(_buf._buffer[index]);
534
535 paste(word + len);
536 for (int i = 0; i < insertLen; i++) {
537 char c = _buf._buffer[i + end];
538 _buf._buffer[i + end] = lastUpper ? toupper(c) : tolower(c);
539 }
540 }
541}
542
543const char *TextEditInput::completeKeyword(int index) {
544 const char *help = nullptr;
545 char *selection = getWordBeforeCursor();
546 if (selection != nullptr) {
547 int len = strlen(selection);
548 int count = 0;
549 for (int i = 0; i < keyword_help_len; i++) {
550 if (strncasecmp(selection, keyword_help[i].keyword, len) == 0 &&
551 count++ == index) {
552 if (IS_WHITE(_buf._buffer[_state.cursor]) || _buf._buffer[_state.cursor] == '\0') {
553 completeWord(keyword_help[i].keyword);
554 }
555 help = keyword_help[i].signature;
556 break;
557 }
558 }
559 free(selection);
560 }
561 return help;
562}
563
564void TextEditInput::draw(int x, int y, int w, int h, int chw) {
565 SyntaxState syntax = kReset;
566 StbTexteditRow r;
567 int len = _buf._len;
568 int i = 0;
569 int baseY = 0;
570 int cursorX = x;
571 int cursorY = y;
572 int cursorMatchX = x;
573 int cursorMatchY = y;
574 int row = 0;
575 int line = 0;
576 int selectStart = MIN(_state.select_start, _state.select_end);
577 int selectEnd = MAX(_state.select_start, _state.select_end);
578
579 maSetColor(_theme->_background);
580 maFillRect(x, y, _width, _height);
581 maSetColor(_theme->_color);
582
583 while (i < len) {
584 layout(&r, i);
585 if (baseY + r.ymax > _height) {
586 break;
587 }
588
589 if (i == 0 ||
590 _buf._buffer[i - 1] == '\r' ||
591 _buf._buffer[i - 1] == '\n') {
592 syntax = kReset;
593 line++;
594 }
595
596 if (row++ >= _scroll) {
597 if (_matchingBrace != -1 && _matchingBrace >= i &&
598 _matchingBrace < i + r.num_chars) {
599 cursorMatchX = x + ((_matchingBrace - i) * chw);
600 cursorMatchY = y + baseY;
601 }
602
603 if ((_state.cursor >= i && _state.cursor < i + r.num_chars) ||
604 (i + r.num_chars == _buf._len && _state.cursor == _buf._len)) {
605 // set cursor position
606 if (_state.cursor == i + r.num_chars &&
607 _buf._buffer[i + r.num_chars - 1] == STB_TEXTEDIT_NEWLINE) {
608 // place cursor on newline
609 cursorX = x;
610 cursorY = y + baseY + _charHeight;
611 } else {
612 cursorX = x + ((_state.cursor - i) * chw);
613 cursorY = y + baseY;
614 }
615 // the logical line, will be < _cursorRow when there are wrapped lines
616 _cursorLine = line;
617
618 if (_marginWidth > 0 && selectStart == selectEnd) {
619 maSetColor(_theme->_row_cursor);
620 maFillRect(x + _marginWidth, cursorY, _width, _charHeight);
621 maSetColor(_theme->_color);
622 }
623 }
624
625 int numChars = getLineChars(&r, i);
626 if (selectStart != selectEnd && i + numChars > selectStart && i < selectEnd) {
627 if (numChars) {
628 // draw selected text
629 int begin = selectStart - i;
630 int baseX = _marginWidth;
631 if (begin > 0) {
632 // initial non-selected chars
633 maSetColor(_theme->_color);
634 maDrawText(x + baseX, y + baseY, _buf._buffer + i, begin);
635 baseX += begin * _charWidth;
636 } else if (begin < 0) {
637 // started on previous row
638 selectStart = i;
639 begin = 0;
640 }
641
642 int count = selectEnd - selectStart;
643 if (count > numChars - begin) {
644 // fill to end of row
645 count = numChars - begin;
646 numChars = 0;
647 }
648
649 maSetColor(_theme->_selection_background);
650 maFillRect(x + baseX, y + baseY, count * _charWidth, _charHeight);
651 maSetColor(_theme->_selection_color);
652 maDrawText(x + baseX, y + baseY, _buf._buffer + i + begin, count);
653
654 int end = numChars - (begin + count);
655 if (end) {
656 // trailing non-selected chars
657 baseX += count * _charWidth;
658 maSetColor(_theme->_color);
659 maDrawText(x + baseX, y + baseY, _buf._buffer + i + begin + count, end);
660 }
661 } else {
662 // draw empty row selection
663 maSetColor(_theme->_selection_background);
664 maFillRect(x + _marginWidth, y + baseY, _charWidth / 2, _charHeight);
665 }
666 drawLineNumber(x, y + baseY, line, true);
667 } else {
668 drawLineNumber(x, y + baseY, line, false);
669 if (numChars) {
670 if (_marginWidth > 0) {
671 drawText(x + _marginWidth, y + baseY, _buf._buffer + i, numChars, syntax);
672 } else {
673 maSetColor(_theme->_color);
674 maDrawText(x + _marginWidth, y + baseY, _buf._buffer + i, numChars);
675 }
676 }
677 }
678 baseY += _charHeight;
679 } else if (row <= _scroll && syntax == kReset) {
680 int end = i + r.num_chars - 1;
681 if (_buf._buffer[end] != '\r' &&
682 _buf._buffer[end] != '\n') {
683 // scrolled line continues to next line
684 for (int j = i; j < end; j++) {
685 if (is_comment(_buf._buffer, j)) {
686 syntax = kComment;
687 break;
688 } else if (_buf._buffer[j] == '\"') {
689 syntax = (syntax == kText) ? kReset : kText;
690 }
691 }
692 }
693 }
694 i += r.num_chars;
695 }
696
697 _bottom = i >= _buf._len;
698 drawLineNumber(x, y + baseY, line + 1, false);
699
700 // draw cursor
701 maSetColor(_theme->_cursor_background);
702 maFillRect(cursorX + _marginWidth, cursorY, chw, _charHeight);
703 if (_state.cursor < _buf._len) {
704 maSetColor(_theme->_cursor_color);
705 if (_buf._buffer[_state.cursor] != '\r' &&
706 _buf._buffer[_state.cursor] != '\n') {
707 maDrawText(cursorX + _marginWidth, cursorY, _buf._buffer + _state.cursor, 1);
708 }
709 }
710 if (_matchingBrace != -1) {
711 maSetColor(_theme->_match_background);
712 maFillRect(cursorMatchX + _marginWidth, cursorMatchY, chw, _charHeight);
713 if (_matchingBrace < _buf._len) {
714 maSetColor(_theme->_cursor_color);
715 maDrawText(cursorMatchX + _marginWidth, cursorMatchY, _buf._buffer + _matchingBrace, 1);
716 }
717 }
718}
719
720void TextEditInput::dragPage(int y, bool &redraw) {
721 int size = abs(y - _ptY);
722 int minSize = _charHeight / 4;
723 if (_ptY == -1) {
724 _ptY = y;
725 } else if (size > minSize) {
726 lineNavigate(y < _ptY);
727 redraw = true;
728 _ptY = y;
729 }
730}
731
732void TextEditInput::drawText(int x, int y, const char *str,
733 int length, SyntaxState &state) {
734 int i = 0;
735 int offs = 0;
736 SyntaxState nextState = state;
737
738 while (offs < length && i < length) {
739 int count = 0;
740 int next = 0;
741 nextState = state;
742
743 // find the end of the current segment
744 while (i < length) {
745 if (state == kComment || is_comment(str, i)) {
746 next = length - i;
747 nextState = kComment;
748 break;
749 } else if (state == kText || str[i] == '\"') {
750 next = 1;
751 while (i + next < length && str[i + next] != '\"') {
752 if (i + next + 1 < length &&
753 str[i + next] == '\\' && str[i + next + 1] == '\"') {
754 next++;
755 }
756 next++;
757 }
758 if (str[i + next] == '\"') {
759 next++;
760 }
761 nextState = kText;
762 break;
763 } else if (state == kReset && isdigit(str[i]) &&
764 (i == 0 || !isalnum(str[i - 1]))) {
765 next = 1;
766 while (i + next < length && isdigit(str[i + next])) {
767 next++;
768 }
769 if (!isalnum(str[i + next])) {
770 if (i > 0 && str[i - 1] == '.') {
771 i--;
772 count--;
773 next++;
774 }
775 nextState = kDigit;
776 break;
777 } else {
778 i += next;
779 count += next;
780 next = 0;
781 }
782 } else if (state == kReset) {
783 int size = 0;
784 uint32_t hash = getHash(str, i, size);
785 if (hash > 0) {
786 if (matchCommand(hash)) {
787 nextState = kCommand;
788 next = size;
789 break;
790 } else if (matchStatement(hash)) {
791 nextState = kStatement;
792 next = size;
793 break;
794 } else if (size > 0) {
795 i += size - 1;
796 count += size - 1;
797 }
798 }
799 }
800 i++;
801 count++;
802 }
803
804 // draw the current segment
805 if (count > 0) {
806 setColor(state);
807 maDrawText(x, y, str + offs, count);
808 offs += count;
809 x += (count * _charWidth);
810 }
811
812 // draw the next segment
813 if (next > 0) {
814 setColor(nextState);
815 maDrawText(x, y, str + offs, next);
816 state = kReset;
817 offs += next;
818 x += (next * _charWidth);
819 i += next;
820 }
821 }
822
823 char cend = str[length];
824 if (cend == '\r' || cend == '\n') {
825 state = kReset;
826 } else {
827 state = nextState;
828 }
829}
830
831bool TextEditInput::edit(int key, int screenWidth, int charWidth) {
832 switch (key) {
833 case SB_KEY_CTRL('a'):
834 selectAll();
835 break;
836 case SB_KEY_ALT('c'):
837 changeCase();
838 break;
839 case SB_KEY_ALT('d'):
840 killWord();
841 break;
842 case SB_KEY_ALT('w'):
843 selectWord();
844 break;
845 case SB_KEY_CTRL('d'):
846 stb_textedit_key(&_buf, &_state, STB_TEXTEDIT_K_DELETE);
847 break;
848 case SB_KEY_CTRL('k'):
849 editDeleteLine();
850 break;
851 case SB_KEY_ALT('n'):
852 removeTrailingSpaces();
853 break;
854 case SB_KEY_ALT('t'):
855 cycleTheme();
856 break;
857 case SB_KEY_TAB:
858 editTab();
859 break;
860 case SB_KEY_CTRL('t'):
861 toggleMarker();
862 break;
863 case SB_KEY_CTRL('g'):
864 gotoNextMarker();
865 break;
866 case SB_KEY_SHIFT(SB_KEY_PGUP):
867 case SB_KEY_PGUP:
868 pageNavigate(false, key == (int)SB_KEY_SHIFT(SB_KEY_PGUP));
869 return true;
870 case SB_KEY_SHIFT(SB_KEY_PGDN):
871 case SB_KEY_PGDN:
872 pageNavigate(true, key == (int)SB_KEY_SHIFT(SB_KEY_PGDN));
873 return true;
874 case SB_KEY_CTRL(SB_KEY_UP):
875 lineNavigate(false);
876 return true;
877 case SB_KEY_CTRL(SB_KEY_DN):
878 lineNavigate(true);
879 return true;
880 case SB_KEY_ENTER:
881 editEnter();
882 break;
883 case SB_KEY_SHIFT_CTRL(SB_KEY_LEFT):
884 selectNavigate(true);
885 break;
886 case SB_KEY_SHIFT_CTRL(SB_KEY_RIGHT):
887 selectNavigate(false);
888 break;
889 case SB_KEY_ALT(SB_KEY_LEFT):
890 case SB_KEY_ALT(SB_KEY_RIGHT):
891 case SB_KEY_ALT(SB_KEY_UP):
892 case SB_KEY_ALT(SB_KEY_DOWN):
893 case SB_KEY_ALT(SB_KEY_ESCAPE):
894 // TODO: block move text selections
895 return false;
896 break;
897 case -1:
898 return false;
899 break;
900 default:
901 stb_textedit_key(&_buf, &_state, key);
902 break;
903 }
904
905 _cursorRow = getCursorRow();
906 if (key == STB_TEXTEDIT_K_UP ||
907 key == (int)SB_KEY_SHIFT(STB_TEXTEDIT_K_UP)) {
908 if (_cursorRow == _scroll) {
909 updateScroll();
910 }
911 } else {
912 int pageRows = _height / _charHeight;
913 if (_cursorRow - _scroll >= pageRows || _cursorRow < _scroll) {
914 // scroll for cursor outside of current frame
915 updateScroll();
916 }
917 }
918 findMatchingBrace();
919 return true;
920}
921
922bool TextEditInput::find(const char *word, bool next) {
923 bool result = false;
924 bool allUpper = true;
925 int len = word == nullptr ? 0 : strlen(word);
926 for (int i = 0; i < len; i++) {
927 if (islower(word[i])) {
928 allUpper = false;
929 break;
930 }
931 }
932
933 if (_buf._buffer != nullptr && word != nullptr) {
934 const char *found = find_str(allUpper, _buf._buffer + _state.cursor, word);
935 if (next && found != nullptr) {
936 // skip to next word
937 found = find_str(allUpper, found + strlen(word), word);
938 }
939 if (found == nullptr) {
940 // start over
941 found = find_str(allUpper, _buf._buffer, word);
942 }
943 if (found != nullptr) {
944 result = true;
945 _state.cursor = found - _buf._buffer;
946 _state.select_start = _state.cursor;
947 _state.select_end = _state.cursor + strlen(word);
948 _cursorRow = getCursorRow();
949 updateScroll();
950 }
951 }
952 return result;
953}
954
955void TextEditInput::getSelectionCounts(int *lines, int *chars) {
956 *lines = 1;
957 *chars = 0;
958 if (_state.select_start != _state.select_end) {
959 int start = MIN(_state.select_start, _state.select_end);
960 int end = MAX(_state.select_start, _state.select_end);
961 int len = _buf._len;
962 StbTexteditRow r;
963
964 *chars = (end - start);
965 for (int i = start; i < end && i < len; i += r.num_chars) {
966 layout(&r, i);
967 if (i + r.num_chars < end) {
968 // found another row before selection end
969 *lines += 1;
970 *chars -= 1;
971 } else if (i + r.num_chars == end) {
972 // cursor at start of next line
973 *chars -= 1;
974 }
975 }
976 }
977}
978
979int TextEditInput::getSelectionRow() {
980 int result;
981 if (_state.select_start != _state.select_end) {
982 int pos = MIN(_state.select_start, _state.select_end);
983 int len = _buf._len;
984 result = 0;
985 StbTexteditRow r;
986 for (int i = 0; i < len; i += r.num_chars) {
987 layout(&r, i);
988 if (pos >= i && pos < i + r.num_chars) {
989 break;
990 }
991 result++;
992 }
993 } else {
994 result = 0;
995 }
996 return result;
997}
998
999char *TextEditInput::getTextSelection(bool selectAll) {
1000 char *result;
1001 if (_state.select_start != _state.select_end) {
1002 int start, end;
1003 if (_state.select_start > _state.select_end) {
1004 end = _state.select_start;
1005 start = _state.select_end;
1006 } else {
1007 start = _state.select_start;
1008 end = _state.select_end;
1009 }
1010 result = _buf.textRange(start, end);
1011 } else if (selectAll) {
1012 result = _buf.textRange(0, _buf._len);
1013 } else {
1014 result = nullptr;
1015 }
1016 return result;
1017}
1018
1019int *TextEditInput::getMarkers() {
1020 return g_lineMarker;
1021}
1022
1023void TextEditInput::gotoLine(const char *buffer) {
1024 if (_buf._buffer != nullptr && buffer != nullptr) {
1025 setCursorRow(atoi(buffer) - 1);
1026 }
1027}
1028
1029void TextEditInput::reload(const char *text) {
1030 _scroll = 0;
1031 _cursorRow = 0;
1032 _buf.clear();
1033 if (text != nullptr) {
1034 _buf.insertChars(0, text, strlen(text));
1035 }
1036 stb_textedit_initialize_state(&_state, false);
1037}
1038
1039bool TextEditInput::save(const char *filePath) {
1040 bool result = true;
1041 FILE *fp = fopen(filePath, "wb");
1042 if (fp) {
1043 fwrite(_buf._buffer, sizeof(char), _buf._len, fp);
1044 fclose(fp);
1045 _dirty = false;
1046 } else {
1047 result = false;
1048 }
1049 return result;
1050}
1051
1052void TextEditInput::selectAll() {
1053 _state.cursor = _state.select_start = 0;
1054 _state.select_end = _buf._len;
1055}
1056
1057void TextEditInput::setCursor(int cursor) {
1058 _state.cursor = lineStart(cursor);
1059 _cursorRow = getCursorRow();
1060 _matchingBrace = -1;
1061 updateScroll();
1062}
1063
1064void TextEditInput::setCursorPos(int pos) {
1065 _state.cursor = pos;
1066 _cursorRow = getCursorRow();
1067 _matchingBrace = -1;
1068 updateScroll();
1069}
1070
1071void TextEditInput::setCursorRow(int row) {
1072 int len = _buf._len;
1073 StbTexteditRow r;
1074 for (int i = 0, nextRow = 0; i < len; i += r.num_chars, nextRow++) {
1075 layout(&r, i);
1076 if (row == nextRow) {
1077 _state.cursor = i;
1078 break;
1079 }
1080 }
1081 _cursorRow = row;
1082 _matchingBrace = -1;
1083 updateScroll();
1084}
1085
1086void TextEditInput::clicked(int x, int y, bool pressed) {
1087 FormEditInput::clicked(x, y, pressed);
1088 if (x < _marginWidth) {
1089 _ptY = -1;
1090 } else if (pressed) {
1091 int tick = dev_get_millisecond_count();
1092 if (_pressTick && tick - _pressTick < DOUBLE_CLICK_MS) {
1093 _state.select_start = wordStart();
1094 _state.select_end = wordEnd();
1095 } else {
1096 stb_textedit_click(&_buf, &_state, (x - _x) - _marginWidth, (y - _y) + (_scroll * _charHeight));
1097 }
1098 _pressTick = tick;
1099 }
1100}
1101
1102void TextEditInput::updateField(var_p_t form) {
1103 var_p_t field = getField(form);
1104 if (field != nullptr) {
1105 var_p_t value = map_get(field, FORM_INPUT_VALUE);
1106 if (value != nullptr) {
1107 v_setstrn(value, _buf._buffer, _buf._len);
1108 }
1109 }
1110}
1111
1112bool TextEditInput::updateUI(var_p_t form, var_p_t field) {
1113 bool updated = (form && field) ? FormInput::updateUI(form, field) : false;
1114 if (!_theme) {
1115 _theme = new EditTheme();
1116 updated = true;
1117 }
1118 return updated;
1119}
1120
1121bool TextEditInput::selected(MAPoint2d pt, int scrollX, int scrollY, bool &redraw) {
1122 bool result = hasFocus() && FormEditInput::selected(pt, scrollX, scrollY, redraw);
1123 if (result) {
1124 if (pt.x < _marginWidth) {
1125 dragPage(pt.y, redraw);
1126 } else {
1127 stb_textedit_drag(&_buf, &_state, (pt.x - _x) - _marginWidth,
1128 (pt.y - _y) + scrollY + (_scroll * _charHeight));
1129 redraw = true;
1130 }
1131 }
1132 return result;
1133}
1134
1135void TextEditInput::selectNavigate(bool up) {
1136 int start = _state.select_start == _state.select_end ? _state.cursor : _state.select_start;
1137 _state.select_start = _state.select_end = _state.cursor;
1138 stb_textedit_key(&_buf, &_state, up ? STB_TEXTEDIT_K_WORDLEFT : STB_TEXTEDIT_K_WORDRIGHT);
1139 _state.select_start = start;
1140 _state.select_end = _state.cursor;
1141}
1142
1143char *TextEditInput::copy(bool cut) {
1144 int selectStart = MIN(_state.select_start, _state.select_end);
1145 int selectEnd = MAX(_state.select_start, _state.select_end);
1146 char *result;
1147 if (selectEnd > selectStart) {
1148 result = _buf.textRange(selectStart, selectEnd);
1149 if (cut) {
1150 stb_textedit_cut(&_buf, &_state);
1151 }
1152 _state.select_start = _state.select_end;
1153 } else {
1154 result = nullptr;
1155 }
1156 return result;
1157}
1158
1159void TextEditInput::paste(const char *text) {
1160 if (text != nullptr) {
1161 int lines = _buf._lines;
1162 stb_textedit_paste(&_buf, &_state, text, strlen(text));
1163 if (lines != _buf._lines) {
1164 _cursorRow = getCursorRow();
1165 updateScroll();
1166 }
1167 }
1168}
1169
1170void TextEditInput::layout(StbTexteditRow *row, int start) const {
1171 int i = start;
1172 int len = _buf._len;
1173 int x1 = 0;
1174 int x2 = _width - _charWidth - _marginWidth;
1175 int numChars = 0;
1176
1177 // advance to newline or rectangle edge
1178 while (i < len
1179 && x1 < x2
1180 && _buf._buffer[i] != '\r'
1181 && _buf._buffer[i] != '\n') {
1182 x1 += _charWidth;
1183 numChars++;
1184 i++;
1185 }
1186
1187 row->num_chars = numChars;
1188 row->x1 = x1;
1189
1190 if (_buf._buffer[i] == '\r') {
1191 // advance over DOS newline
1192 row->num_chars++;
1193 i++;
1194 }
1195 if (_buf._buffer[i] == '\n') {
1196 // advance over newline
1197 row->num_chars++;
1198 }
1199 row->x0 = 0.0f;
1200 row->ymin = 0.0f;
1201 row->ymax = row->baseline_y_delta = _charHeight;
1202}
1203
1204void TextEditInput::layout(int w, int h) {
1205 if (_resizable) {
1206 _width = w - (_x + _xmargin);
1207 _height = h - (_y + _ymargin);
1208 }
1209}
1210
1211int TextEditInput::charWidth(int k, int i) const {
1212 int result = 0;
1213 if (k + i < _buf._len && _buf._buffer[k + i] != '\n') {
1214 result = _charWidth;
1215 }
1216 return result;
1217}
1218
1219void TextEditInput::calcMargin() {
1220 MAExtent screenSize = maGetScrSize();
1221 _xmargin = EXTENT_X(screenSize) - (_x + _width);
1222 _ymargin = EXTENT_Y(screenSize) - (_y + _height);
1223}
1224
1225void TextEditInput::changeCase() {
1226 int start, end;
1227 char *selection = getSelection(&start, &end);
1228 int len = strlen(selection);
1229 enum { up, down, mixed } curcase = isupper(selection[0]) ? up : down;
1230
1231 for (int i = 1; i < len; i++) {
1232 if (isalpha(selection[i])) {
1233 bool isup = isupper(selection[i]);
1234 if ((curcase == up && isup == false) || (curcase == down && isup)) {
1235 curcase = mixed;
1236 break;
1237 }
1238 }
1239 }
1240
1241 // transform pattern: Foo -> FOO, FOO -> foo, foo -> Foo
1242 for (int i = 0; i < len; i++) {
1243 selection[i] = curcase == mixed ? toupper(selection[i]) : tolower(selection[i]);
1244 }
1245 if (curcase == down) {
1246 selection[0] = toupper(selection[0]);
1247 // upcase chars following non-alpha chars
1248 for (int i = 1; i < len; i++) {
1249 if (isalpha(selection[i]) == false && i + 1 < len) {
1250 selection[i + 1] = toupper(selection[i + 1]);
1251 }
1252 }
1253 }
1254 if (selection[0]) {
1255 _state.select_start = start;
1256 _state.select_end = end;
1257 stb_textedit_paste(&_buf, &_state, selection, strlen(selection));
1258 }
1259 free(selection);
1260}
1261
1262void TextEditInput::cycleTheme() {
1263 g_themeId = (g_themeId + 1) % NUM_THEMES;
1264 _theme->selectTheme(themes[g_themeId]);
1265}
1266
1267void TextEditInput::drawLineNumber(int x, int y, int row, bool selected) {
1268 if (_marginWidth > 0) {
1269 bool markerRow = false;
1270 for (int i = 0; i < MAX_MARKERS && !markerRow; i++) {
1271 if (row == g_lineMarker[i]) {
1272 markerRow = true;
1273 }
1274 }
1275 if (markerRow) {
1276 maSetColor(_theme->_row_marker);
1277 } else if (selected) {
1278 maSetColor(_theme->_number_selection_background);
1279 maFillRect(x, y, _marginWidth, _charHeight);
1280 maSetColor(_theme->_number_selection_color);
1281 } else {
1282 maSetColor(_theme->_number_color);
1283 }
1284 int places = 0;
1285 for (int n = row; n > 0; n /= 10) {
1286 places++;
1287 }
1288 char rowBuffer[places + 1];
1289 int offs = (_marginWidth - (_charWidth * places)) / 2;
1290
1291 sprintf(rowBuffer, "%d", row);
1292 maDrawText(x + offs, y, rowBuffer, places);
1293 }
1294}
1295
1296void TextEditInput::editDeleteLine() {
1297 int start = _state.cursor;
1298 int end = linePos(_state.cursor, true, true);
1299 if (end > start) {
1300 // delete the entire line when the cursor is at the home position
1301 stb_textedit_delete(&_buf, &_state, start, end - start + (_cursorCol == 0 ? 1 : 0));
1302 _state.cursor = start;
1303 } else if (start == end) {
1304 stb_textedit_delete(&_buf, &_state, start, 1);
1305 }
1306}
1307
1308void TextEditInput::editEnter() {
1309 stb_textedit_key(&_buf, &_state, STB_TEXTEDIT_NEWLINE);
1310 int start = lineStart(_state.cursor);
1311 int prevLineStart = lineStart(start - 1);
1312
1313 if (prevLineStart || _cursorLine == 1) {
1314 char spaces[LINE_BUFFER_SIZE];
1315 int indent = getIndent(spaces, LINE_BUFFER_SIZE, prevLineStart);
1316
1317 // check whether the previous line was a comment
1318 char *buf = lineText(prevLineStart);
1319 int length = strlen(buf);
1320 int pos = 0;
1321 while (buf && (buf[pos] == ' ' || buf[pos] == '\t')) {
1322 pos++;
1323 }
1324 if (length > 2 && (buf[pos] == '#' || buf[pos] == '\'') && indent + 2 < LINE_BUFFER_SIZE) {
1325 spaces[indent] = buf[pos];
1326 spaces[++indent] = ' ';
1327 spaces[++indent] = '\0';
1328 } else if (length > 4 && strncasecmp(buf + pos, "rem", 3) == 0) {
1329 indent = strlcat(spaces, "rem ", LINE_BUFFER_SIZE);
1330 }
1331 free(buf);
1332
1333 if (indent) {
1334 _buf.insertChars(_state.cursor, spaces, indent);
1335 stb_text_makeundo_insert(&_state, _state.cursor, indent);
1336 _state.cursor += indent;
1337 }
1338 }
1339}
1340
1341void TextEditInput::editTab() {
1342 char spaces[LINE_BUFFER_SIZE];
1343
1344 // get the desired indent based on the previous line
1345 int start = lineStart(_state.cursor);
1346 int prevLineStart = lineStart(start - 1);
1347
1348 if (prevLineStart && prevLineStart + 1 == start) {
1349 // allows for a single blank line between statements
1350 prevLineStart = lineStart(prevLineStart - 1);
1351 }
1352 // note - spaces not used in this context
1353 int indent = (prevLineStart || _cursorLine == 2) ? getIndent(spaces, sizeof(spaces), prevLineStart) : 0;
1354
1355 // get the current lines indent
1356 char *buf = lineText(start);
1357 int curIndent = 0;
1358 while (buf && (buf[curIndent] == ' ' || buf[curIndent] == '\t')) {
1359 curIndent++;
1360 }
1361
1362 // adjust indent for statement terminators
1363 if (indent >= _indentLevel && buf && endStatement(buf + curIndent)) {
1364 indent -= _indentLevel;
1365 }
1366 if (curIndent < indent) {
1367 // insert additional spaces
1368 int len = indent - curIndent;
1369 if (len > (int)sizeof(spaces) - 1) {
1370 len = (int)sizeof(spaces) - 1;
1371 }
1372 memset(spaces, ' ', len);
1373 spaces[len] = 0;
1374 _buf.insertChars(start, spaces, len);
1375 stb_text_makeundo_insert(&_state, start, len);
1376
1377 if (_state.cursor - start < indent) {
1378 // jump cursor to start of text
1379 _state.cursor = start + indent;
1380 } else {
1381 // move cursor along with text movement, staying on same line
1382 int maxpos = lineEnd(start);
1383 if (_state.cursor + len <= maxpos) {
1384 _state.cursor += len;
1385 }
1386 }
1387 } else if (curIndent > indent) {
1388 // remove excess spaces
1389 stb_textedit_delete(&_buf, &_state, start, curIndent - indent);
1390 _state.cursor = start + indent;
1391 } else if (start + indent > 0) {
1392 // already have ideal indent - soft-tab to indent
1393 _state.cursor = start + indent;
1394 }
1395 free(buf);
1396}
1397
1398bool TextEditInput::endStatement(const char *buf) {
1399 static const struct Holder {
1400 const char *symbol;
1401 int len;
1402 } term[] = {
1403 {"wend", 4},
1404 {"fi", 2},
1405 {"endif", 5},
1406 {"elseif ", 7},
1407 {"elif ", 5},
1408 {"else", 4},
1409 {"next", 4},
1410 {"case", 4},
1411 {"end", 3},
1412 {"until ", 6}
1413 };
1414 static const int len = sizeof(term) / sizeof(Holder);
1415 bool result = false;
1416 for (int i = 0; i < len && !result; i++) {
1417 if (strncasecmp(buf, term[i].symbol, term[i].len) == 0) {
1418 char c = buf[term[i].len];
1419 if (c == '\0' || IS_WHITE(c)) {
1420 result = true;
1421 }
1422 }
1423 }
1424 return result;
1425}
1426
1427void TextEditInput::findMatchingBrace() {
1428 char cursorChar = _state.cursor < _buf._len ? _buf._buffer[_state.cursor] : '\0';
1429 char cursorMatch = '\0';
1430 int pair = -1;
1431 int iter = -1;
1432 int pos;
1433
1434 switch (cursorChar) {
1435 case ']':
1436 cursorMatch = '[';
1437 pos = _state.cursor - 1;
1438 break;
1439 case ')':
1440 cursorMatch = '(';
1441 pos = _state.cursor - 1;
1442 break;
1443 case '(':
1444 cursorMatch = ')';
1445 pos = _state.cursor + 1;
1446 iter = 1;
1447 break;
1448 case '[':
1449 cursorMatch = ']';
1450 iter = 1;
1451 pos = _state.cursor + 1;
1452 break;
1453 }
1454 if (cursorMatch != '\0') {
1455 // scan for matching opening on the same line
1456 int level = 1;
1457 int len = _buf._len;
1458 int gap = 0;
1459 while (pos > 0 && pos < len) {
1460 char nextChar = _buf._buffer[pos];
1461 if (nextChar == '\0' || nextChar == '\n') {
1462 break;
1463 }
1464 if (nextChar == cursorChar) {
1465 // nested char
1466 level++;
1467 } else if (nextChar == cursorMatch) {
1468 level--;
1469 if (level == 0) {
1470 // found matching char at pos
1471 if (gap > 0) {
1472 pair = pos;
1473 }
1474 break;
1475 }
1476 }
1477 pos += iter;
1478 gap++;
1479 }
1480 }
1481 _matchingBrace = pair;
1482}
1483
1484int TextEditInput::getCompletions(StringList *list, int max) {
1485 int count = 0;
1486 char *selection = getWordBeforeCursor();
1487 unsigned len = selection != nullptr ? strlen(selection) : 0;
1488 if (len > 0) {
1489 for (int i = 0; i < keyword_help_len && count < max; i++) {
1490 if (strncasecmp(selection, keyword_help[i].keyword, len) == 0) {
1491 String *s = new String();
1492 s->append(" ");
1493 s->append(keyword_help[i].keyword);
1494 list->add(s);
1495 count++;
1496 }
1497 }
1498 }
1499 free(selection);
1500 return count;
1501}
1502
1503int TextEditInput::getCursorRow() {
1504 StbTexteditRow r;
1505 int len = _buf._len;
1506 int row = 0;
1507 int i;
1508
1509 for (i = 0; i < len;) {
1510 layout(&r, i);
1511 if (_state.cursor == i + r.num_chars &&
1512 _buf._buffer[i + r.num_chars - 1] == STB_TEXTEDIT_NEWLINE) {
1513 // at end of line
1514 row++;
1515 _cursorCol = 0;
1516 break;
1517 } else if (_state.cursor >= i && _state.cursor < i + r.num_chars) {
1518 // within line
1519 _cursorCol = _state.cursor - i;
1520 break;
1521 }
1522 i += r.num_chars;
1523 if (i < len) {
1524 row++;
1525 }
1526 }
1527 return row;
1528}
1529
1530uint32_t TextEditInput::getHash(const char *str, int offs, int &count) {
1531 uint32_t result = 0;
1532 if ((offs == 0 || IS_WHITE(str[offs - 1]) || ispunct(str[offs - 1]))
1533 && !IS_WHITE(str[offs]) && str[offs] != '\0') {
1534 for (count = 0; count < keyword_max_len; count++) {
1535 char ch = str[offs + count];
1536 if (ch == '.') {
1537 // could be SELF keyword
1538 break;
1539 } else if (!isalpha(ch) && ch != '_') {
1540 // non keyword character
1541 break;
1542 }
1543 result += tolower(str[offs + count]);
1544 result += (result << 4);
1545 result ^= (result >> 2);
1546 }
1547 }
1548 return result;
1549}
1550
1551int TextEditInput::getIndent(char *spaces, int len, int pos) {
1552 // count the indent level and find the start of text
1553 char *buf = lineText(pos);
1554 int i = 0;
1555 while (i < len && (buf[i] == ' ' || buf[i] == '\t')) {
1556 spaces[i] = buf[i];
1557 i++;
1558 }
1559
1560 if (strncasecmp(buf + i, "while ", 6) == 0 ||
1561 strncasecmp(buf + i, "if ", 3) == 0 ||
1562 strncasecmp(buf + i, "elseif ", 7) == 0 ||
1563 strncasecmp(buf + i, "elif ", 5) == 0 ||
1564 strncasecmp(buf + i, "else", 4) == 0 ||
1565 strncasecmp(buf + i, "repeat", 6) == 0 ||
1566 strncasecmp(buf + i, "for ", 4) == 0 ||
1567 strncasecmp(buf + i, "select ", 7) == 0 ||
1568 strncasecmp(buf + i, "case ", 5) == 0 ||
1569 strncasecmp(buf + i, "sub ", 4) == 0 ||
1570 strncasecmp(buf + i, "func ", 5) == 0) {
1571
1572 // handle if-then-blah on same line
1573 if (strncasecmp(buf + i, "if ", 3) == 0) {
1574 // find the end of line index
1575 int j = i + 4;
1576 while (buf[j] != 0 && buf[j] != '\n') {
1577 // line also 'ends' at start of comments
1578 if (is_comment(buf, j)) {
1579 break;
1580 }
1581 j++;
1582 }
1583 // right trim trailing spaces
1584 while ((buf[j - 1] == ' ' || buf[j - 1] == '\t') && j > i) {
1585 j--;
1586 }
1587 if (strncasecmp(buf + j - 4, "then", 4) != 0) {
1588 // 'then' is not final text on line
1589 spaces[i] = 0;
1590 free(buf);
1591 return i;
1592 }
1593 }
1594 // indent new line
1595 for (int j = 0; j < _indentLevel; j++, i++) {
1596 spaces[i] = ' ';
1597 }
1598 }
1599 spaces[i] = 0;
1600 free(buf);
1601 return i;
1602}
1603
1604int TextEditInput::getLineChars(StbTexteditRow *row, int pos) {
1605 int numChars = row->num_chars;
1606 if (numChars > 0 && _buf._buffer[pos + row->num_chars - 1] == '\r') {
1607 numChars--;
1608 }
1609 if (numChars > 0 && _buf._buffer[pos + row->num_chars - 1] == '\n') {
1610 numChars--;
1611 }
1612 return numChars;
1613}
1614
1615char *TextEditInput::getSelection(int *start, int *end) {
1616 char *result;
1617
1618 if (_state.select_start != _state.select_end) {
1619 result = _buf.textRange(_state.select_start, _state.select_end);
1620 *start = _state.select_start;
1621 *end = _state.select_end;
1622 } else {
1623 *start = wordStart();
1624 *end = wordEnd();
1625 result = _buf.textRange(*start, *end);
1626 }
1627 return result;
1628}
1629
1630const char *TextEditInput::getNodeId() {
1631 char *selection = getWordBeforeCursor();
1632 const char *result = nullptr;
1633 int len = selection != nullptr ? strlen(selection) : 0;
1634 if (len > 0) {
1635 for (int i = 0; i < keyword_help_len && !result; i++) {
1636 if (strcasecmp(selection, keyword_help[i].keyword) == 0) {
1637 result = keyword_help[i].nodeId;
1638 }
1639 }
1640 }
1641 free(selection);
1642 return result;
1643}
1644
1645char *TextEditInput::getWordBeforeCursor() {
1646 char *result;
1647 if (_state.select_start == _state.select_end && _buf._len > 0) {
1648 int start, end;
1649 result = getSelection(&start, &end);
1650 } else {
1651 result = nullptr;
1652 }
1653 return result;
1654}
1655
1656bool TextEditInput::replaceNext(const char *buffer, bool skip) {
1657 bool changed = false;
1658 if (_state.select_start != _state.select_end &&
1659 _buf._buffer != nullptr && buffer != nullptr) {
1660 int start, end;
1661 char *selection = getSelection(&start, &end);
1662 if (!skip) {
1663 stb_textedit_paste(&_buf, &_state, buffer, strlen(buffer));
1664 } else {
1665 _state.cursor++;
1666 }
1667 changed = find(selection, false);
1668 free(selection);
1669 }
1670 return changed;
1671}
1672
1673void TextEditInput::gotoNextMarker() {
1674 int next = 0;
1675 int first = -1;
1676 for (int i = 0; i < MAX_MARKERS; i++) {
1677 if (g_lineMarker[i] != -1) {
1678 if (first == -1) {
1679 first = i;
1680 }
1681 if (g_lineMarker[i] == _cursorLine) {
1682 next = i + 1 == MAX_MARKERS ? first : i + 1;
1683 break;
1684 }
1685 }
1686 }
1687 if (first != -1) {
1688 if (g_lineMarker[next] == -1) {
1689 next = first;
1690 }
1691 if (g_lineMarker[next] != -1) {
1692 setCursorRow(g_lineMarker[next] - 1);
1693 }
1694 }
1695}
1696
1697void TextEditInput::killWord() {
1698 int start = _state.cursor;
1699 int end = wordEnd();
1700 if (start == end) {
1701 int word = textedit_move_to_word_next(&_buf, _state.cursor);
1702 end = textedit_move_to_word_next(&_buf, word) - 1;
1703 int bound = lineEnd(start);
1704 if (end > bound && bound != start) {
1705 // clip to line end when there are characters prior to the line end
1706 end = bound;
1707 }
1708 }
1709 if (end > start) {
1710 stb_textedit_delete(&_buf, &_state, start, end - start);
1711 }
1712}
1713
1714void TextEditInput::lineNavigate(bool arrowDown) {
1715 if (arrowDown) {
1716 if (!_bottom) {
1717 StbTexteditRow r;
1718 layout(&r, _state.cursor);
1719 _state.cursor += r.num_chars;
1720 _scroll += 1;
1721 }
1722 } else if (_scroll > 0) {
1723 int newLines = 0;
1724 int i = _state.cursor - 1;
1725 while (i > 0) {
1726 if (_buf._buffer[i] == '\n' && ++newLines == 2) {
1727 // scan to before the previous line, then add 1
1728 break;
1729 }
1730 i--;
1731 }
1732 _state.cursor = i == 0 ? 0 : i + 1;
1733 _scroll -= 1;
1734 }
1735}
1736
1737char *TextEditInput::lineText(int pos) {
1738 StbTexteditRow r;
1739 int len = _buf._len;
1740 int start = 0;
1741 int end = 0;
1742 for (int i = 0; i < len; i += r.num_chars) {
1743 layout(&r, i);
1744 if (pos >= i && pos < i + r.num_chars) {
1745 start = i;
1746 end = i + getLineChars(&r, i);
1747 break;
1748 }
1749 }
1750 return _buf.textRange(start, end);
1751}
1752
1753int TextEditInput::linePos(int pos, bool end, bool excludeBreak) {
1754 StbTexteditRow r;
1755 int len = _buf._len;
1756 int start = 0;
1757 for (int i = 0; i < len; i += r.num_chars) {
1758 layout(&r, i);
1759 if (pos >= i && pos < i + r.num_chars) {
1760 if (end) {
1761 if (excludeBreak) {
1762 start = i + getLineChars(&r, i);
1763 } else {
1764 start = i + r.num_chars;
1765 }
1766 } else {
1767 start = i;
1768 }
1769 break;
1770 }
1771 }
1772 return start;
1773}
1774
1775bool TextEditInput::matchCommand(uint32_t hash) {
1776 bool result = false;
1777 for (int i = 0; i < keyword_hash_command_len && !result; i++) {
1778 if (keyword_hash_command[i] == hash) {
1779 result = true;
1780 }
1781 }
1782 return result;
1783}
1784
1785bool TextEditInput::matchStatement(uint32_t hash) {
1786 bool result = false;
1787 for (int i = 0; i < keyword_hash_statement_len && !result; i++) {
1788 if (keyword_hash_statement[i] == hash) {
1789 result = true;
1790 }
1791 }
1792 return result;
1793}
1794
1795void TextEditInput::pageNavigate(bool pageDown, bool shift) {
1796 int pageRows = (_height / _charHeight) + 1;
1797 int nextRow = _cursorRow + (pageDown ? pageRows : -pageRows);
1798 if (nextRow < 0) {
1799 nextRow = 0;
1800 }
1801
1802 StbTexteditRow r;
1803 int len = _buf._len;
1804 int row = 0;
1805 int i = 0;
1806 int count = 0;
1807
1808 for (; i < len && row != nextRow; i += r.num_chars, row++) {
1809 layout(&r, i);
1810 count += r.num_chars;
1811 }
1812
1813 if (count == _buf._len) {
1814 // at end
1815 row--;
1816 i -= r.num_chars;
1817 }
1818
1819 if (shift) {
1820 if (_state.select_start == _state.select_end) {
1821 _state.select_start = _state.cursor;
1822 }
1823 _state.select_end = i;
1824 } else {
1825 _state.select_start = _state.select_end;
1826 }
1827
1828 _state.cursor = i;
1829 _cursorRow = row;
1830 _cursorCol = 0;
1831 updateScroll();
1832}
1833
1834void TextEditInput::removeTrailingSpaces() {
1835 int row = getCursorRow();
1836 _buf.removeTrailingSpaces(&_state);
1837 setCursorRow(row - 1);
1838}
1839
1840void TextEditInput::selectWord() {
1841 if (_state.select_start != _state.select_end) {
1842 // advance to next word
1843 _state.cursor = textedit_move_to_word_next(&_buf, _state.cursor);
1844 _state.select_start = _state.select_end = -1;
1845 }
1846 _state.select_start = wordStart();
1847 _state.select_end = _state.cursor = wordEnd();
1848
1849 if (_state.select_start == _state.select_end) {
1850 // move to next word
1851 _state.cursor = textedit_move_to_word_next(&_buf, _state.cursor);
1852 }
1853}
1854
1855void TextEditInput::setColor(SyntaxState &state) {
1856 switch (state) {
1857 case kComment:
1858 maSetColor(_theme->_syntax_comments);
1859 break;
1860 case kText:
1861 maSetColor(_theme->_syntax_text);
1862 break;
1863 case kCommand:
1864 maSetColor(_theme->_syntax_command);
1865 break;
1866 case kStatement:
1867 maSetColor(_theme->_syntax_statement);
1868 break;
1869 case kDigit:
1870 maSetColor(_theme->_syntax_digit);
1871 break;
1872 case kReset:
1873 maSetColor(_theme->_color);
1874 break;
1875 }
1876}
1877
1878void TextEditInput::toggleMarker() {
1879 bool found = false;
1880 for (int i = 0; i < MAX_MARKERS && !found; i++) {
1881 if (_cursorLine == g_lineMarker[i]) {
1882 g_lineMarker[i] = -1;
1883 found = true;
1884 }
1885 }
1886 if (!found) {
1887 for (int i = 0; i < MAX_MARKERS && !found; i++) {
1888 if (g_lineMarker[i] == -1) {
1889 g_lineMarker[i] = _cursorLine;
1890 found = true;
1891 break;
1892 }
1893 }
1894 }
1895 if (!found) {
1896 g_lineMarker[0] = _cursorLine;
1897 }
1898 qsort(g_lineMarker, MAX_MARKERS, sizeof(int), compareIntegers);
1899}
1900
1901void TextEditInput::updateScroll() {
1902 int pageRows = _height / _charHeight;
1903 if (_cursorRow + 1 < pageRows) {
1904 _scroll = 0;
1905 } else if (_cursorRow >= _scroll + pageRows || _cursorRow <= _scroll) {
1906 // cursor outside current view
1907 _scroll = _cursorRow - (pageRows / 2);
1908 }
1909}
1910
1911int TextEditInput::wordEnd() {
1912 int i = _state.cursor;
1913 while (i >= 0 && i < _buf._len && IS_VAR_CHAR(_buf._buffer[i])) {
1914 i++;
1915 }
1916 return i;
1917}
1918
1919int TextEditInput::wordStart() {
1920 int cursor = _state.cursor == 0 ? 0 : _state.cursor - 1;
1921 return ((cursor >= 0 && cursor < _buf._len && _buf._buffer[cursor] == '\n') ? _state.cursor :
1922 is_word_border(&_buf, _state.cursor) ? _state.cursor :
1923 textedit_move_to_word_previous(&_buf, _state.cursor));
1924}
1925
1926//
1927// TextEditHelpWidget
1928//
1929TextEditHelpWidget::TextEditHelpWidget(TextEditInput *editor, int chW, int chH, bool overlay) :
1930 TextEditInput(nullptr, chW, chH, editor->_x, editor->_y, editor->_width, editor->_height),
1931 _mode(kNone),
1932 _editor(editor),
1933 _openPackage(nullptr),
1934 _openKeyword(-1),
1935 _layout(kPopup) {
1936 _theme = new EditTheme(HELP_FG, HELP_BG);
1937 hide();
1938 if (overlay) {
1939 _x = editor->_width - (chW * HELP_WIDTH);
1940 _width = chW * HELP_WIDTH;
1941 }
1942}
1943
1944TextEditHelpWidget::~TextEditHelpWidget() {
1945 _outline.clear();
1946}
1947
1948bool TextEditHelpWidget::closeOnEnter() const {
1949 return (_mode != kSearch && _mode != kHelpKeyword);
1950}
1951
1952bool TextEditHelpWidget::edit(int key, int screenWidth, int charWidth) {
1953 bool result = false;
1954 switch (_mode) {
1955 case kSearch:
1956 result = TextEditInput::edit(key, screenWidth, charWidth);
1957 _editor->find(_buf._buffer, key == SB_KEY_ENTER);
1958 break;
1959 case kEnterReplace:
1960 result = TextEditInput::edit(key, screenWidth, charWidth);
1961 if (key == SB_KEY_ENTER) {
1962 _buf.clear();
1963 _mode = kEnterReplaceWith;
1964 } else {
1965 _editor->find(_buf._buffer, false);
1966 }
1967 break;
1968 case kEnterReplaceWith:
1969 if (key == SB_KEY_ENTER) {
1970 _mode = kReplace;
1971 } else {
1972 result = TextEditInput::edit(key, screenWidth, charWidth);
1973 }
1974 break;
1975 case kReplace:
1976 switch (key) {
1977 case SB_KEY_ENTER:
1978 if (!_editor->replaceNext(_buf._buffer, false)) {
1979 _mode = kReplaceDone;
1980 }
1981 break;
1982 case ' ':
1983 if (!_editor->replaceNext(_buf._buffer, true)) {
1984 // skip to next
1985 _mode = kReplaceDone;
1986 }
1987 break;
1988 }
1989 break;
1990 case kGotoLine:
1991 result = TextEditInput::edit(key, screenWidth, charWidth);
1992 if (key == SB_KEY_ENTER) {
1993 _editor->gotoLine(_buf._buffer);
1994 }
1995 break;
1996 case kLineEdit:
1997 if (key != SB_KEY_ENTER) {
1998 result = TextEditInput::edit(key, screenWidth, charWidth);
1999 }
2000 break;
2001 case kMessage:
2002 // readonly mode
2003 break;
2004 default:
2005 switch (key) {
2006 case STB_TEXTEDIT_K_LEFT:
2007 case STB_TEXTEDIT_K_RIGHT:
2008 case STB_TEXTEDIT_K_UP:
2009 case STB_TEXTEDIT_K_DOWN:
2010 case STB_TEXTEDIT_K_PGUP:
2011 case STB_TEXTEDIT_K_PGDOWN:
2012 case STB_TEXTEDIT_K_LINESTART:
2013 case STB_TEXTEDIT_K_LINEEND:
2014 case STB_TEXTEDIT_K_TEXTSTART:
2015 case STB_TEXTEDIT_K_TEXTEND:
2016 case STB_TEXTEDIT_K_WORDLEFT:
2017 case STB_TEXTEDIT_K_WORDRIGHT:
2018 result = TextEditInput::edit(key, screenWidth, charWidth);
2019 if (_mode == kOutline && _cursorRow < _outline.size()) {
2020 int cursor = (intptr_t)_outline[_cursorRow];
2021 _editor->setCursor(cursor);
2022 } else if (_mode == kStacktrace && _cursorRow < _outline.size()) {
2023 int cursorRow = (intptr_t)_outline[_cursorRow];
2024 _editor->setCursorRow(cursorRow - 1);
2025 }
2026 break;
2027 case SB_KEY_ENTER:
2028 switch (_mode) {
2029 case kCompletion:
2030 completeWord(_state.cursor);
2031 break;
2032 case kHelpKeyword:
2033 toggleKeyword();
2034 break;
2035 default:
2036 break;
2037 }
2038 result = true;
2039 break;
2040 default:
2041 if (_mode == kHelpKeyword && _openKeyword != -1 && key < 0) {
2042 result = TextEditInput::edit(key, screenWidth, charWidth);
2043 }
2044 break;
2045 }
2046 }
2047 return result;
2048}
2049
2050void TextEditHelpWidget::completeLine(int pos) {
2051 int end = pos;
2052 while (end < _buf._len && _buf._buffer[end] != '\n') {
2053 end++;
2054 }
2055 char *text = _buf.textRange(pos, end);
2056 if (text[0] != '\0' && text[0] != '[') {
2057 _editor->completeWord(text);
2058 }
2059 free(text);
2060}
2061
2062void TextEditHelpWidget::completeWord(int pos) {
2063 char *text = lineText(pos);
2064 if (text[0] != '\0' && text[0] != '[') {
2065 _editor->completeWord(text);
2066 }
2067 free(text);
2068}
2069
2070void TextEditHelpWidget::clicked(int x, int y, bool pressed) {
2071 _ptY = -1;
2072 if (pressed) {
2073 stb_textedit_click(&_buf, &_state, 0, (y - _y) + (_scroll * _charHeight));
2074 if (_mode == kHelpKeyword && (x - _x) <= _charWidth * 3) {
2075 toggleKeyword();
2076 }
2077 }
2078}
2079
2080void TextEditHelpWidget::createCompletionHelp() {
2081 reset(kCompletion);
2082
2083 char *selection = _editor->getWordBeforeCursor();
2084 int len = selection != nullptr ? strlen(selection) : 0;
2085 if (len > 0) {
2086 StringList words;
2087 for (int i = 0; i < keyword_help_len; i++) {
2088 if (strncasecmp(selection, keyword_help[i].keyword, len) == 0) {
2089 words.add(keyword_help[i].keyword);
2090 _buf.append(keyword_help[i].keyword);
2091 _buf.append("\n", 1);
2092 }
2093 }
2094 const char *text = _editor->getText();
2095 const char *found = strcasestr(text, selection);
2096 while (found != nullptr) {
2097 const char *end = found;
2098 const char pre = found > text ? *(found - 1) : ' ';
2099 while (IS_VAR_CHAR(*end) && *end != '\0') {
2100 end++;
2101 }
2102 if (end - found > len && (IS_WHITE(pre) || pre == '.')) {
2103 String next;
2104 next.append(found, end - found);
2105 if (!words.contains(next)) {
2106 words.add(next);
2107 _buf.append(found, end - found);
2108 _buf.append("\n", 1);
2109 }
2110 }
2111 found = strcasestr(end, selection);
2112 }
2113 } else {
2114 const char *package = nullptr;
2115 for (int i = 0; i < keyword_help_len; i++) {
2116 if (package == nullptr || strcasecmp(package, keyword_help[i].package) != 0) {
2117 // next package
2118 package = keyword_help[i].package;
2119 _buf.append("[");
2120 _buf.append(package);
2121 _buf.append("]\n");
2122 }
2123 _buf.append(keyword_help[i].keyword);
2124 _buf.append("\n", 1);
2125 }
2126 }
2127 free(selection);
2128}
2129
2130void TextEditHelpWidget::createGotoLine() {
2131 reset(kGotoLine);
2132}
2133
2134void TextEditHelpWidget::createHelp() {
2135 reset(kHelp);
2136 _buf.append(helpText, strlen(helpText));
2137}
2138
2139void TextEditHelpWidget::createLineEdit(const char *value) {
2140 reset(kLineEdit);
2141 if (value && value[0]) {
2142 _buf.append(value, strlen(value));
2143 }
2144}
2145
2146void TextEditHelpWidget::createKeywordIndex() {
2147 char *keyword = _editor->getWordBeforeCursor();
2148 reset(kHelpKeyword);
2149
2150 bool keywordFound = false;
2151 if (keyword != nullptr) {
2152 for (int i = 0; i < keyword_help_len && !keywordFound; i++) {
2153 if (strcasecmp(keyword, keyword_help[i].keyword) == 0) {
2154 _buf.append(TWISTY2_OPEN, TWISTY2_LEN);
2155 _buf.append(keyword_help[i].keyword);
2156 _openPackage = keyword_help[i].package;
2157 keywordFound = true;
2158 toggleKeyword();
2159 break;
2160 }
2161 }
2162 free(keyword);
2163 }
2164
2165 if (!keywordFound) {
2166 const char *package = nullptr;
2167 for (int i = 0; i < keyword_help_len; i++) {
2168 if (package == nullptr || strcasecmp(package, keyword_help[i].package) != 0) {
2169 package = keyword_help[i].package;
2170 _buf.append(TWISTY1_OPEN, TWISTY1_LEN);
2171 _buf.append(package);
2172 _buf.append("\n", 1);
2173 }
2174 }
2175 }
2176}
2177
2178void TextEditHelpWidget::createOutline() {
2179 const char *text = _editor->getText();
2180 int len = _editor->getTextLength();
2181 const char *keywords[] = {
2182 "sub ", "func ", "def ", "label ", "const ", "local ", "dim "
2183 };
2184 int keywords_length = sizeof(keywords) / sizeof(keywords[0]);
2185 int keywords_len[keywords_length];
2186 for (int j = 0; j < keywords_length; j++) {
2187 keywords_len[j] = strlen(keywords[j]);
2188 }
2189
2190 reset(kOutline);
2191
2192 int cursorPos = _editor->getCursorPos();
2193
2194 for (int i = 0; i < len; i++) {
2195 // skip to the newline start
2196 while (i < len && i != 0 && text[i] != '\n') {
2197 i++;
2198 }
2199
2200 // skip any successive newlines
2201 while (i < len && text[i] == '\n') {
2202 i++;
2203 }
2204
2205 // skip any leading whitespace
2206 while (i < len && (text[i] == ' ' || text[i] == '\t')) {
2207 i++;
2208 }
2209
2210 int iNext = i;
2211
2212 for (int j = 0; j < keywords_length; j++) {
2213 if (!strncasecmp(text + i, keywords[j], keywords_len[j])) {
2214 i += keywords_len[j];
2215 int iBegin = i;
2216 while (i < len && text[i] != '=' && text[i] != '\r' && text[i] != '\n') {
2217 i++;
2218 }
2219 if (i > iBegin) {
2220 int numChars = i - iBegin;
2221 int padding = j > 1 ? 4 : 2;
2222 if (numChars > HELP_WIDTH - padding) {
2223 numChars = HELP_WIDTH - padding;
2224 }
2225 if (numChars > 0) {
2226 if (j > 1) {
2227 _buf.append(" .", 2);
2228 }
2229 _buf.append(text + iBegin, numChars);
2230 _buf.append("\n", 1);
2231 _outline.add((int *)(intptr_t)i);
2232 }
2233 }
2234 break;
2235 }
2236 }
2237
2238 if (iNext < i && cursorPos < i && !_state.cursor) {
2239 _state.cursor = _buf._len - 1;
2240 }
2241
2242 if (text[i] == '\n') {
2243 // avoid eating the entire next line
2244 i--;
2245 }
2246 }
2247
2248 _state.cursor = lineStart(_state.cursor);
2249 _cursorRow = getCursorRow();
2250 updateScroll();
2251}
2252
2253void TextEditHelpWidget::createSearch(bool replace) {
2254 if (_mode != kSearch) {
2255 reset(replace ? kEnterReplace : kSearch);
2256 }
2257
2258 char *text = _editor->getTextSelection(false);
2259 if (text != nullptr) {
2260 // prime search from selected text
2261 _buf.clear();
2262 _buf.insertChars(0, text, strlen(text));
2263 free(text);
2264
2265 // ensure the selected word is first match
2266 _editor->setCursorPos(_editor->getSelectionStart());
2267 }
2268}
2269
2270void TextEditHelpWidget::createStackTrace(const char *error, int line, StackTrace &trace) {
2271 reset(kStacktrace);
2272
2273 _outline.add((int *)(intptr_t)line);
2274 _buf.append("Error:\n");
2275
2276 List_each(StackTraceNode *, it, trace) {
2277 StackTraceNode *node = (*it);
2278 _outline.add((int *)(intptr_t)node->_line);
2279 _buf.append(" ", 1);
2280 _buf.append(node->_keyword);
2281 _buf.append("\n", 1);
2282 }
2283
2284 _buf.append("\n", 1);
2285 _buf.append(error);
2286 _buf.append("\n", 1);
2287 _outline.add((int *)(intptr_t)line);
2288 _outline.add((int *)(intptr_t)line);
2289}
2290
2291void TextEditHelpWidget::paste(const char *text) {
2292 switch (_mode) {
2293 case kSearch:
2294 case kEnterReplace:
2295 case kEnterReplaceWith:
2296 case kLineEdit:
2297 TextEditInput::paste(text);
2298 break;
2299 default:
2300 break;
2301 }
2302}
2303
2304void TextEditHelpWidget::reset(HelpMode mode) {
2305 stb_textedit_clear_state(&_state, mode == kSearch);
2306 _outline.clear();
2307 _mode = mode;
2308 _buf.clear();
2309 _scroll = 0;
2310 _matchingBrace = -1;
2311}
2312
2313bool TextEditHelpWidget::selected(MAPoint2d pt, int scrollX, int scrollY, bool &redraw) {
2314 bool result = hasFocus();
2315 if (result) {
2316 dragPage(pt.y, redraw);
2317 }
2318 return result;
2319}
2320
2321void TextEditHelpWidget::toggleKeyword() {
2322 char *line = lineText(_state.cursor);
2323 bool open1 = strncmp(line, TWISTY1_OPEN, TWISTY1_LEN) == 0;
2324 bool open2 = strncmp(line, TWISTY2_OPEN, TWISTY2_LEN) == 0;
2325 bool close1 = strncmp(line, TWISTY1_CLOSE, TWISTY1_LEN) == 0;
2326 bool close2 = strncmp(line, TWISTY2_CLOSE, TWISTY2_LEN) == 0;
2327 if (open1 || open2 || close1 || close2) {
2328 const char *nextLine = line + TWISTY1_LEN;
2329 const char *package = (open2 || close2) && _openPackage != nullptr ? _openPackage : nextLine;
2330 const char *nextPackage = nullptr;
2331 int pageRows = _height / _charHeight;
2332 int open1Count = 0;
2333 int open2Count = 0;
2334 _buf.clear();
2335 _matchingBrace = -1;
2336 _openKeyword = -1;
2337 _state.select_start = _state.select_end = 0;
2338
2339 for (int i = 0; i < keyword_help_len; i++) {
2340 if (nextPackage == nullptr || strcasecmp(nextPackage, keyword_help[i].package) != 0) {
2341 nextPackage = keyword_help[i].package;
2342 if (strcasecmp(package, nextPackage) == 0) {
2343 // selected item
2344 if (open1 || close1) {
2345 _state.cursor = _buf._len;
2346 _cursorRow = open1Count;
2347 }
2348
2349 _buf.append(open1 || open2 || close2 ? TWISTY1_CLOSE : TWISTY1_OPEN, TWISTY1_LEN);
2350 _buf.append(nextPackage);
2351 _buf.append("\n", 1);
2352
2353 if (open1) {
2354 _openPackage = nextPackage;
2355 open1Count++;
2356 } else if (open2) {
2357 nextLine = line + TWISTY2_LEN;
2358 open2Count++;
2359 }
2360 if (open1 || open2 || close2) {
2361 while (i < keyword_help_len &&
2362 strcasecmp(nextPackage, keyword_help[i].package) == 0) {
2363 open2Count++;
2364 if (open2 && strcasecmp(nextLine, keyword_help[i].keyword) == 0) {
2365 _openKeyword = i;
2366 _state.cursor = _buf._len;
2367 _cursorRow = open1Count + open2Count;
2368 _buf.append(TWISTY2_CLOSE, TWISTY2_LEN);
2369 _buf.append(keyword_help[i].keyword);
2370 _buf.append("\n\n", 2);
2371 _buf.append(keyword_help[i].signature);
2372 _buf.append("\n\n", 2);
2373 _buf.append(keyword_help[i].help);
2374 _buf.append("\n\n", 2);
2375 } else {
2376 _buf.append(TWISTY2_OPEN, TWISTY2_LEN);
2377 _buf.append(keyword_help[i].keyword);
2378 _buf.append("\n", 1);
2379 }
2380 i++;
2381 }
2382 }
2383 } else {
2384 // next package item (level 1)
2385 _buf.append(TWISTY1_OPEN, TWISTY1_LEN);
2386 _buf.append(nextPackage);
2387 _buf.append("\n", 1);
2388 open1Count++;
2389 }
2390 }
2391 }
2392 if (_cursorRow + 4 < pageRows) {
2393 _scroll = 0;
2394 } else {
2395 _scroll = _cursorRow - (pageRows / 4);
2396 }
2397 }
2398 free(line);
2399}
2400
2401void TextEditHelpWidget::showPopup(int cols, int rows) {
2402 if (cols < 0) {
2403 _width = _editor->_width - (_charWidth * -cols);
2404 } else {
2405 _width = _charWidth * cols;
2406 }
2407 if (rows < 0) {
2408 _height = _editor->_height - (_charHeight * -rows);
2409 } else {
2410 _height = _charHeight * rows;
2411 }
2412 if (_width > _editor->_width) {
2413 _width = _editor->_width;
2414 }
2415 if (_height > _editor->_height) {
2416 _height = _editor->_height;
2417 }
2418 _x = (_editor->_width - _width) / 2;
2419 if (rows == 1) {
2420 _layout = kLine;
2421 _y = _editor->_height - (_charHeight * 2.5);
2422 } else {
2423 _layout = kPopup;
2424 _y = (_editor->_height - _height) / 2;
2425 }
2426 _theme->contrast(_editor->getTheme());
2427 calcMargin();
2428 show();
2429}
2430
2431void TextEditHelpWidget::showSidebar() {
2432 int border = _charWidth * 2;
2433 _width = _charWidth * SIDE_BAR_WIDTH;
2434 _height = _editor->_height - (border * 2);
2435 _x = _editor->_width - (_width + border);
2436 _y = border;
2437 _theme->contrast(_editor->getTheme());
2438 _layout = kSidebar;
2439 calcMargin();
2440 show();
2441}
2442
2443void TextEditHelpWidget::draw(int x, int y, int w, int h, int chw) {
2444 TextEditInput::draw(x, y, w, h, chw);
2445 int shadowW = _charWidth / 3;
2446 int shadowH = _charWidth / 3;
2447
2448 maSetColor(_theme->_selection_background);
2449 maFillRect(x + _width, y + shadowH, shadowW, _height);
2450 maFillRect(x + shadowW, y + _height, _width, shadowH);
2451}
2452
2453void TextEditHelpWidget::layout(int w, int h) {
2454 if (_resizable) {
2455 int border;
2456 switch (_layout) {
2457 case kLine:
2458 _x = (w - _width) / 2;
2459 _y = h - (_charHeight * 2.5);
2460 break;
2461 case kSidebar:
2462 border = _charWidth * 2;
2463 _height = h - (border * 2);
2464 _x = w - (_width + border);
2465 break;
2466 case kPopup:
2467 _width = w - (_x + _xmargin);
2468 _height = h - (_y + _ymargin);
2469 }
2470 }
2471}
2472