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 <config.h>
10#include <stdint.h>
11#include <FL/Fl_Rect.H>
12#include "platform/fltk/BasicEditor.h"
13#include "platform/fltk/kwp.h"
14#include "platform/fltk/Profile.h"
15
16using namespace strlib;
17
18Fl_Color defaultColor[] = {
19 FL_BLACK, // A - Plain
20 fl_rgb_color(0, 128, 0), // B - Comments
21 fl_rgb_color(0, 0, 192), // C - Strings
22 fl_rgb_color(192, 0, 0), // D - code_keywords
23 fl_rgb_color(128, 128, 0), // E - code_functions
24 fl_rgb_color(0, 128, 128), // F - code_procedures
25 fl_rgb_color(128, 0, 128), // G - Find matches
26 fl_rgb_color(0, 128, 0), // H - Italic Comments '
27 fl_rgb_color(0, 128, 128), // I - Numbers
28 fl_rgb_color(128, 128, 64), // J - Operators
29};
30
31Fl_Text_Display::Style_Table_Entry styletable[] = {
32 { defaultColor[0], FL_COURIER, 12}, // A - Plain
33 { defaultColor[1], FL_COURIER, 12}, // B - Comments
34 { defaultColor[2], FL_COURIER, 12}, // C - Strings
35 { defaultColor[3], FL_COURIER, 12}, // D - code_keywords
36 { defaultColor[4], FL_COURIER, 12}, // E - code_functions
37 { defaultColor[5], FL_COURIER, 12}, // F - code_procedures
38 { defaultColor[6], FL_COURIER, 12}, // G - Find matches
39 { defaultColor[7], FL_COURIER_ITALIC, 12}, // H - Italic Comments
40 { defaultColor[8], FL_COURIER, 12}, // I - Numbers
41 { defaultColor[9], FL_COURIER, 12}, // J - Operators
42 { FL_BLUE, FL_COURIER, 12}, // K - Selection Background
43 { FL_WHITE, FL_COURIER, 12}, // L - Background
44};
45
46#define PLAIN 'A'
47#define COMMENTS 'B'
48#define STRINGS 'C'
49#define KEYWORDS 'D'
50#define FUNCTIONS 'E'
51#define PROCEDURES 'F'
52#define FINDMATCH 'G'
53#define ITCOMMENTS 'H'
54#define DIGITS 'I'
55#define OPERATORS 'J'
56
57/**
58 * return whether the character is a valid variable symbol
59 */
60bool isvar(int c) {
61 return (isalnum(c) || c == '_');
62}
63
64/**
65 * Compare two keywords
66 */
67int compare_keywords(const void *a, const void *b) {
68 return (strcasecmp(*((const char **)a), *((const char **)b)));
69}
70
71/**
72 * Update unfinished styles.
73 */
74void style_unfinished_cb(int, void *) {
75}
76
77/**
78 * Update the style buffer
79 */
80void style_update_cb(int pos, // I - Position of update
81 int nInserted, // I - Number of inserted chars
82 int nDeleted, // I - Number of deleted chars
83 int /* nRestyled */ , // I - Number of restyled chars
84 const char * /* deletedText */ , // I - Text that was deleted
85 void *cbArg) { // I - Callback data
86 BasicEditor *editor = (BasicEditor *) cbArg;
87 Fl_Text_Buffer *stylebuf = editor->_stylebuf;
88 Fl_Text_Buffer *textbuf = editor->_textbuf;
89
90 // if this is just a selection change, just unselect the style buffer
91 if (nInserted == 0 && nDeleted == 0) {
92 stylebuf->unselect();
93 return;
94 }
95 // track changes in the text buffer
96 if (nInserted > 0) {
97 // insert characters into the style buffer
98 char *stylex = new char[nInserted + 1];
99 memset(stylex, PLAIN, nInserted);
100 stylex[nInserted] = '\0';
101 stylebuf->replace(pos, pos + nDeleted, stylex);
102 delete[]stylex;
103 } else {
104 // just delete characters in the style buffer
105 stylebuf->remove(pos, pos + nDeleted);
106 }
107
108 // Select the area that was just updated to avoid unnecessary callbacks
109 stylebuf->select(pos, pos + nInserted - nDeleted);
110
111 // re-parse the changed region; we do this by parsing from the
112 // beginning of the line of the changed region to the end of
113 // the line of the changed region Then we check the last
114 // style character and keep updating if we have a multi-line
115 // comment character
116 if (nInserted > 0) {
117 int start = textbuf->line_start(pos);
118 int end = textbuf->line_end(pos + nInserted);
119 char *text_range = textbuf->text_range(start, end);
120 char *style_range = stylebuf->text_range(start, end);
121 int last = style_range[end - start - 1];
122
123 editor->styleParse(text_range, style_range, end - start);
124 stylebuf->replace(start, end, style_range);
125 editor->redisplay_range(start, end);
126
127 if (last != style_range[end - start - 1]) {
128 // the last character on the line changed styles,
129 // so reparse the remainder of the buffer
130 free(text_range);
131 free(style_range);
132 end = textbuf->length();
133 text_range = textbuf->text_range(start, end);
134 style_range = stylebuf->text_range(start, end);
135 editor->styleParse(text_range, style_range, end - start);
136 stylebuf->replace(start, end, style_range);
137 editor->redisplay_range(start, end);
138 }
139 free(text_range);
140 free(style_range);
141 }
142}
143
144//--BasicEditor------------------------------------------------------------------
145
146BasicEditor::BasicEditor(int x, int y, int w, int h, StatusBar *status) :
147 Fl_Text_Editor(x, y, w, h),
148 _readonly(false),
149 _status(status) {
150
151 const char *s = getenv("INDENT_LEVEL");
152 _indentLevel = (s && s[0] ? atoi(s) : 2);
153 _matchingBrace = -1;
154 _textbuf = new Fl_Text_Buffer();
155 _stylebuf = new Fl_Text_Buffer();
156 _search[0] = 0;
157 highlight_data(_stylebuf, styletable,
158 sizeof(styletable) / sizeof(styletable[0]),
159 PLAIN, style_unfinished_cb, 0);
160 _textbuf->add_modify_callback(style_update_cb, this);
161 buffer(_textbuf);
162}
163
164BasicEditor::~BasicEditor() {
165 buffer(nullptr);
166 delete _textbuf;
167 delete _stylebuf;
168}
169
170/**
171 * Parse text and produce style data.
172 */
173void BasicEditor::styleParse(const char *text, char *style, int length) {
174 char current = PLAIN;
175 int last = 0; // prev char was alpha-num
176 char buf[1024];
177 char *bufptr;
178 const char *temp;
179 int searchLen = strlen(_search);
180
181 for (int index = 0; length > 0; length--, text++, index++) {
182 if (current == PLAIN) {
183 // check for directives, comments, strings, and keywords
184 if ((*text == '#' && (index == 0 || *(text - 1) == 0 || *(text - 1) == 10)) ||
185 (strncasecmp(text, "rem", 3) == 0 && text[3] == ' ') || *text == '\'') {
186 // basic comment
187 current = COMMENTS;
188 for (; length > 0 && *text != '\n'; length--, text++) {
189 if (*text == ';') {
190 current = ITCOMMENTS;
191 }
192 *style++ = current;
193 }
194 if (length == 0) {
195 break;
196 }
197 } else if (strncmp(text, "\\\"", 2) == 0) {
198 // quoted quote
199 *style++ = current;
200 *style++ = current;
201 text++;
202 length--;
203 continue;
204 } else if (*text == '\"') {
205 current = STRINGS;
206 } else if (!last) {
207 // begin keyword/number search at non-alnum boundary
208
209 // test for digit sequence
210 if (isdigit(*text)) {
211 *style++ = DIGITS;
212 if (*text == '0' && *(text + 1) == 'x') {
213 // hex number
214 *style++ = DIGITS;
215 text++;
216 length--;
217 }
218 while (*text && (*(text + 1) == '.' || isdigit(*(text + 1)))) {
219 *style++ = DIGITS;
220 text++;
221 length--;
222 }
223 continue;
224 }
225 // test for a keyword
226 temp = text;
227 bufptr = buf;
228 while (*temp != 0 && *temp != ' ' &&
229 *temp != '\n' && *temp != '\r' && *temp != '"' &&
230 *temp != '(' && *temp != ')' && *temp != '=' && bufptr < (buf + sizeof(buf) - 1)) {
231 *bufptr++ = tolower(*temp++);
232 }
233
234 *bufptr = '\0';
235 bufptr = buf;
236
237 if (searchLen > 0) {
238 const char *sfind = strstr(bufptr, _search);
239 // find text match
240 if (sfind != 0) {
241 int offset = sfind - bufptr;
242 style += offset;
243 text += offset;
244 length -= offset;
245 for (int i = 0; i < searchLen && text < temp; i++) {
246 *style++ = FINDMATCH;
247 text++;
248 length--;
249 }
250 text--;
251 length++;
252 last = 1;
253 continue;
254 }
255 }
256
257 if (bsearch(&bufptr, code_keywords, code_keywords_len, sizeof(code_keywords[0]), compare_keywords)) {
258 while (text < temp) {
259 *style++ = KEYWORDS;
260 text++;
261 length--;
262 }
263 text--;
264 length++;
265 last = 1;
266 continue;
267 } else if (bsearch(&bufptr, code_functions, code_functions_len,
268 sizeof(code_functions[0]), compare_keywords)) {
269 while (text < temp) {
270 *style++ = FUNCTIONS;
271 text++;
272 length--;
273 }
274 text--;
275 length++;
276 last = 1;
277 continue;
278 } else if (bsearch(&bufptr, code_procedures, code_procedures_len,
279 sizeof(code_procedures[0]), compare_keywords)) {
280 while (text < temp) {
281 *style++ = PROCEDURES;
282 text++;
283 length--;
284 }
285 text--;
286 length++;
287 last = 1;
288 continue;
289 }
290 }
291 } else if (current == STRINGS) {
292 // continuing in string
293 if (strncmp(text, "\\\"", 2) == 0) {
294 // quoted end quote
295 *style++ = current;
296 *style++ = current;
297 text++;
298 length--;
299 continue;
300 } else if (*text == '\"') {
301 // End quote
302 *style++ = current;
303 current = PLAIN;
304 continue;
305 }
306 }
307 // copy style info
308 *style++ = current;
309 last = isvar(*text) || *text == '.';
310
311 if (*text == '\n') {
312 current = PLAIN; // basic lines do not continue
313 }
314 }
315}
316
317/**
318 * handler for the style change event
319 */
320void BasicEditor::styleChanged() {
321 _textbuf->select(0, _textbuf->length());
322 _textbuf->select(0, 0);
323 damage(FL_DAMAGE_ALL);
324}
325
326/**
327 * display the editor buffer
328 */
329void BasicEditor::draw() {
330 Fl_Text_Editor::draw();
331 if (_matchingBrace != -1) {
332 // highlight the matching brace
333 int X, Y;
334 int cursor = cursor_style();
335 cursor_style(BLOCK_CURSOR);
336 if (position_to_xy(_matchingBrace, &X, &Y)) {
337 draw_cursor(X, Y);
338 }
339 cursor_style(cursor);
340 }
341}
342
343/**
344 * returns the indent position level
345 */
346unsigned BasicEditor::getIndent(char *spaces, int len, int pos) {
347 // count the indent level and find the start of text
348 char *buf = buffer()->line_text(pos);
349 int i = 0;
350 while (buf && buf[i] == ' ' && i < len) {
351 spaces[i] = buf[i];
352 i++;
353 }
354
355 if (strncasecmp(buf + i, "while ", 6) == 0 ||
356 strncasecmp(buf + i, "if ", 3) == 0 ||
357 strncasecmp(buf + i, "elseif ", 7) == 0 ||
358 strncasecmp(buf + i, "elif ", 5) == 0 ||
359 strncasecmp(buf + i, "else", 4) == 0 ||
360 strncasecmp(buf + i, "repeat", 6) == 0 ||
361 strncasecmp(buf + i, "for ", 4) == 0 ||
362 strncasecmp(buf + i, "select ", 7) == 0 ||
363 strncasecmp(buf + i, "case ", 5) == 0 ||
364 strncasecmp(buf + i, "sub ", 4) == 0 ||
365 strncasecmp(buf + i, "func ", 5) == 0) {
366
367 // handle if-then-blah on same line
368 if (strncasecmp(buf + i, "if ", 3) == 0) {
369 // find the end of line index
370 int j = i + 4;
371 while (buf[j] != 0 && buf[j] != '\n') {
372 // line also 'ends' at start of comments
373 if (strncasecmp(buf + j, "rem", 3) == 0 || buf[j] == '\'') {
374 break;
375 }
376 j++;
377 }
378 // right trim trailing spaces
379 while (buf[j - 1] == ' ' && j > i) {
380 j--;
381 }
382 if (strncasecmp(buf + j - 4, "then", 4) != 0) {
383 // 'then' is not final text on line
384 spaces[i] = 0;
385 return i;
386 }
387 }
388 // indent new line
389 for (int j = 0; j < _indentLevel; j++, i++) {
390 spaces[i] = ' ';
391 }
392 }
393 spaces[i] = 0;
394 free((void *)buf);
395 return i;
396}
397
398/**
399 * handler for the TAB character
400 */
401void BasicEditor::handleTab() {
402 char spaces[250];
403 int indent;
404
405 // get the desired indent based on the previous line
406 int lineStart = buffer()->line_start(insert_position());
407 int prevLineStart = buffer()->line_start(lineStart - 1);
408
409 if (prevLineStart && prevLineStart + 1 == lineStart) {
410 // allows for a single blank line between statments
411 prevLineStart = buffer()->line_start(prevLineStart - 1);
412 }
413 // note - spaces not used in this context
414 indent = prevLineStart == 0 ? 0 : getIndent(spaces, sizeof(spaces), prevLineStart);
415
416 // get the current lines indent
417 char *buf = buffer()->line_text(lineStart);
418 int curIndent = 0;
419 while (buf && buf[curIndent] == ' ') {
420 curIndent++;
421 }
422
423 // adjust indent for closure statements
424 if (strncasecmp(buf + curIndent, "wend", 4) == 0 ||
425 strncasecmp(buf + curIndent, "fi", 2) == 0 ||
426 strncasecmp(buf + curIndent, "endif", 5) == 0 ||
427 strncasecmp(buf + curIndent, "elseif ", 7) == 0 ||
428 strncasecmp(buf + curIndent, "elif ", 5) == 0 ||
429 strncasecmp(buf + curIndent, "else", 4) == 0 ||
430 strncasecmp(buf + curIndent, "next", 4) == 0 ||
431 strncasecmp(buf + curIndent, "case", 4) == 0 ||
432 strncasecmp(buf + curIndent, "end", 3) == 0 ||
433 strncasecmp(buf + curIndent, "until ", 6) == 0) {
434 if (indent >= _indentLevel) {
435 indent -= _indentLevel;
436 }
437 }
438 if (curIndent < indent) {
439 // insert additional spaces
440 int len = indent - curIndent;
441 if (len > (int)sizeof(spaces) - 1) {
442 len = (int)sizeof(spaces) - 1;
443 }
444 memset(spaces, ' ', len);
445 spaces[len] = 0;
446 buffer()->insert(lineStart, spaces);
447 if (insert_position() - lineStart < indent) {
448 // jump cursor to start of text
449 insert_position(lineStart + indent);
450 } else {
451 // move cursor along with text movement, staying on same line
452 int maxpos = buffer()->line_end(lineStart);
453 if (insert_position() + len <= maxpos) {
454 insert_position(insert_position() + len);
455 }
456 }
457 } else if (curIndent > indent) {
458 // remove excess spaces
459 buffer()->remove(lineStart, lineStart + (curIndent - indent));
460 } else {
461 // already have ideal indent - soft-tab to indent
462 insert_position(lineStart + indent);
463 }
464 free((void *)buf);
465}
466
467/**
468 * sets the current display font
469 */
470void BasicEditor::setFont(Fl_Font font) {
471 if (font) {
472 int len = sizeof(styletable) / sizeof(styletable[0]);
473 for (int i = 0; i < len; i++) {
474 styletable[i].font = font;
475 }
476 styleChanged();
477 }
478}
479
480/**
481 * sets the current font size
482 */
483void BasicEditor::setFontSize(int size) {
484 int len = sizeof(styletable) / sizeof(styletable[0]);
485 for (int i = 0; i < len; i++) {
486 styletable[i].size = size;
487 }
488 styleChanged();
489}
490
491/**
492 * display the matching brace
493 */
494void BasicEditor::showMatchingBrace() {
495 char cursorChar = buffer()->char_at(insert_position() - 1);
496 char cursorMatch = 0;
497 int pair = -1;
498 int iter = -1;
499 int pos = insert_position() - 2;
500
501 switch (cursorChar) {
502 case ']':
503 cursorMatch = '[';
504 break;
505 case ')':
506 cursorMatch = '(';
507 break;
508 case '(':
509 cursorMatch = ')';
510 pos = insert_position();
511 iter = 1;
512 break;
513 case '[':
514 cursorMatch = ']';
515 iter = 1;
516 pos = insert_position();
517 break;
518 }
519 if (cursorMatch != -0) {
520 // scan for matching opening on the same line
521 int level = 1;
522 int len = buffer()->length();
523 int gap = 0;
524 while (pos > 0 && pos < len) {
525 char nextChar = buffer()->char_at(pos);
526 if (nextChar == 0 || nextChar == '\n') {
527 break;
528 }
529 if (nextChar == cursorChar) {
530 level++; // nested char
531 } else if (nextChar == cursorMatch) {
532 level--;
533 if (level == 0) {
534 // found matching char at pos
535 if (gap > 1) {
536 pair = pos;
537 }
538 break;
539 }
540 }
541 pos += iter;
542 gap++;
543 }
544 }
545
546 if (_matchingBrace != -1) {
547 int lineStart = buffer()->line_start(_matchingBrace);
548 int lineEnd = buffer()->line_end(_matchingBrace);
549 redisplay_range(lineStart, lineEnd);
550 _matchingBrace = -1;
551 }
552 if (pair != -1) {
553 redisplay_range(pair, pair);
554 _matchingBrace = pair;
555 }
556}
557
558/**
559 * highlight the given search text
560 */
561void BasicEditor::showFindText(const char *find) {
562 // copy lowercase search string for high-lighting
563 strcpy(_search, find);
564 int findLen = strlen(_search);
565
566 for (int i = 0; i < findLen; i++) {
567 _search[i] = tolower(_search[i]);
568 }
569
570 style_update_cb(0, _textbuf->length(), _textbuf->length(), 0, 0, this);
571}
572
573/**
574 * FLTK event handler
575 */
576int BasicEditor::handle(int e) {
577 int cursor_pos = insert_position();
578 bool navigateKey = false;
579
580 switch (Fl::event_key()) {
581 case FL_Home:
582 case FL_Left:
583 case FL_Up:
584 case FL_Right:
585 case FL_Down:
586 case FL_Page_Up:
587 case FL_Page_Down:
588 case FL_End:
589 navigateKey = true;
590 }
591
592 if (_readonly && ((e == FL_KEYBOARD && !navigateKey) || e == FL_PASTE)) {
593 // prevent buffer modification when in readonly state
594 return 0;
595 }
596
597 if (e == FL_KEYBOARD && Fl::event_key() == FL_Tab) {
598 if (Fl::event_state(FL_CTRL)) {
599 // pass ctrl+key to parent
600 return 0;
601 }
602 handleTab();
603 return 1; // skip default handler
604 }
605 // call default handler then process keys
606 int rtn = Fl_Text_Editor::handle(e);
607 switch (e) {
608 case FL_KEYBOARD:
609 if (Fl::event_key() == FL_Enter) {
610 char spaces[250];
611 int indent = getIndent(spaces, sizeof(spaces), cursor_pos);
612 if (indent) {
613 buffer()->insert(insert_position(), spaces);
614 insert_position(insert_position() + indent);
615 damage(FL_DAMAGE_ALL);
616 }
617 }
618 // fallthru to show row-col
619 case FL_RELEASE:
620 showMatchingBrace();
621 showRowCol();
622 break;
623 }
624
625 return rtn;
626}
627
628/**
629 * displays the current row and col position
630 */
631void BasicEditor::showRowCol() {
632 int row = -1;
633 int col = 0;
634
635 if (!position_to_linecol(insert_position(), &row, &col)) {
636 // This is a workaround for a bug in the FLTK TextDisplay widget
637 // where linewrapping causes a mis-calculation of line offsets which
638 // sometimes prevents the display of the last few lines of text.
639 insert_position(0);
640 scroll(0, 0);
641 insert_position(buffer()->length());
642 scroll(count_lines(0, buffer()->length(), 1), 0);
643 position_to_linecol(insert_position(), &row, &col);
644 }
645
646 _status->setRowCol(row, col + 1);
647}
648
649/**
650 * sets the cursor to the given line number
651 */
652void BasicEditor::gotoLine(int line) {
653 int numLines = buffer()->count_lines(0, buffer()->length());
654 if (line < 1) {
655 line = 1;
656 } else if (line > numLines) {
657 line = numLines;
658 }
659 int pos = buffer()->skip_lines(0, line - 1); // find pos at line-1
660 insert_position(buffer()->line_start(pos)); // insert at column 0
661 show_insert_position();
662 _status->setRowCol(line, 1);
663 scroll(line, hor_offset());
664}
665
666/**
667 * returns where text selection starts
668 */
669void BasicEditor::getSelStartRowCol(int *row, int *col) {
670 int start = buffer()->primary_selection()->start();
671 int end = buffer()->primary_selection()->end();
672 if (start == end) {
673 *row = -1;
674 *col = -1;
675 } else {
676 position_to_linecol(start, row, col);
677 }
678}
679
680/**
681 * returns where text selection ends
682 */
683void BasicEditor::getSelEndRowCol(int *row, int *col) {
684 int start = buffer()->primary_selection()->start();
685 int end = buffer()->primary_selection()->end();
686 if (start == end) {
687 *row = -1;
688 *col = -1;
689 } else {
690 position_to_linecol(end, row, col);
691 }
692}
693
694/**
695 * return the selected text and its coordinate rectangle
696 */
697char *BasicEditor::getSelection(Fl_Rect *rc) {
698 char *result = 0;
699 if (!_readonly) {
700 int x1, y1, x2, y2, start, end;
701
702 if (_textbuf->selected()) {
703 _textbuf->selection_position(&start, &end);
704 } else {
705 int pos = insert_position();
706 if (isvar(_textbuf->char_at(pos))) {
707 start = _textbuf->word_start(pos);
708 end = _textbuf->word_end(pos);
709 } else {
710 start = end = 0;
711 }
712 }
713
714 if (start != end) {
715 position_to_xy(start, &x1, &y1);
716 position_to_xy(end, &x2, &y2);
717
718 rc->x(x1);
719 rc->y(y1);
720 rc->w(x2 - x1);
721 rc->h(maxSize());
722 result = _textbuf->text_range(start, end);
723 }
724 }
725 return result;
726}
727
728/**
729 * returns the current font size
730 */
731int BasicEditor::getFontSize() {
732 return (int)styletable[0].size;
733}
734
735/**
736 * returns the current font face name
737 */
738Fl_Font BasicEditor::getFont() {
739 return styletable[0].font;
740}
741
742/**
743 * returns the BASIC keyword list
744 */
745void BasicEditor::getKeywords(strlib::List<String *> &keywords) {
746 for (int i = 0; i < code_keywords_len; i++) {
747 keywords.add(new String(code_keywords[i]));
748 }
749
750 for (int i = 0; i < code_functions_len; i++) {
751 keywords.add(new String(code_functions[i]));
752 }
753
754 for (int i = 0; i < code_procedures_len; i++) {
755 keywords.add(new String(code_procedures[i]));
756 }
757}
758
759/**
760 * returns the row and col position for the current cursor position
761 */
762void BasicEditor::getRowCol(int *row, int *col) {
763 position_to_linecol(insert_position(), row, col);
764}
765
766/**
767 * find text within the editor buffer
768 */
769bool BasicEditor::findText(const char *find, bool forward, bool updatePos) {
770 showFindText(find);
771
772 bool found = false;
773 if (find != 0 && find[0] != 0) {
774 int pos = insert_position();
775 found = forward ? _textbuf->search_forward(pos, _search, &pos) :
776 _textbuf->search_backward(pos - strlen(find), _search, &pos);
777 if (found && updatePos) {
778 _textbuf->select(pos, pos + strlen(_search));
779 insert_position(pos + strlen(_search));
780 show_insert_position();
781 }
782 }
783 return found;
784}
785
786