Implement Ctrl+HOME/Ctrl+END in edit.
[helenos.git] / uspace / app / edit / edit.c
blobbabfbcec72e114d8c22843ef6a5bf66288aa98ca
1 /*
2 * Copyright (c) 2009 Jiri Svoboda
3 * Copyright (c) 2012 Martin Sucha
4 * All rights reserved.
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
10 * - Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
12 * - Redistributions in binary form must reproduce the above copyright
13 * notice, this list of conditions and the following disclaimer in the
14 * documentation and/or other materials provided with the distribution.
15 * - The name of the author may not be used to endorse or promote products
16 * derived from this software without specific prior written permission.
18 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
19 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
20 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
21 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
22 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
23 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
24 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
25 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
27 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 /** @addtogroup edit
31 * @brief Text editor.
32 * @{
34 /**
35 * @file
38 #include <stdio.h>
39 #include <stdlib.h>
40 #include <sys/types.h>
41 #include <vfs/vfs.h>
42 #include <io/console.h>
43 #include <io/style.h>
44 #include <io/keycode.h>
45 #include <errno.h>
46 #include <align.h>
47 #include <macros.h>
48 #include <clipboard.h>
49 #include <bool.h>
51 #include "sheet.h"
52 #include "search.h"
54 enum redraw_flags {
55 REDRAW_TEXT = (1 << 0),
56 REDRAW_ROW = (1 << 1),
57 REDRAW_STATUS = (1 << 2),
58 REDRAW_CARET = (1 << 3)
61 /** Pane
63 * A rectangular area of the screen used to edit a document. Different
64 * panes can be possibly used to edit the same document.
66 typedef struct {
67 /* Pane dimensions */
68 int rows, columns;
70 /* Position of the visible area */
71 int sh_row, sh_column;
73 /** Bitmask of components that need redrawing */
74 enum redraw_flags rflags;
76 /** Current position of the caret */
77 tag_t caret_pos;
79 /** Start of selection */
80 tag_t sel_start;
82 /**
83 * Ideal column where the caret should try to get. This is used
84 * for maintaining the same column during vertical movement.
86 int ideal_column;
88 char *previous_search;
89 bool previous_search_reverse;
90 } pane_t;
92 /** Document
94 * Associates a sheet with a file where it can be saved to.
96 typedef struct {
97 char *file_name;
98 sheet_t *sh;
99 } doc_t;
101 static console_ctrl_t *con;
102 static doc_t doc;
103 static bool done;
104 static pane_t pane;
105 static bool cursor_visible;
107 static sysarg_t scr_rows;
108 static sysarg_t scr_columns;
110 #define ROW_BUF_SIZE 4096
111 #define BUF_SIZE 64
112 #define TAB_WIDTH 8
113 #define ED_INFTY 65536
115 /** Maximum filename length that can be entered. */
116 #define INFNAME_MAX_LEN 128
118 static void cursor_show(void);
119 static void cursor_hide(void);
120 static void cursor_setvis(bool visible);
122 static void key_handle_unmod(kbd_event_t const *ev);
123 static void key_handle_ctrl(kbd_event_t const *ev);
124 static void key_handle_shift(kbd_event_t const *ev);
125 static void key_handle_shift_ctrl(kbd_event_t const *ev);
126 static void key_handle_movement(unsigned int key, bool shift);
128 static int file_save(char const *fname);
129 static void file_save_as(void);
130 static int file_insert(char *fname);
131 static int file_save_range(char const *fname, spt_t const *spos,
132 spt_t const *epos);
133 static char *range_get_str(spt_t const *spos, spt_t const *epos);
135 static char *prompt(char const *prompt, char const *init_value);
137 static void pane_text_display(void);
138 static void pane_row_display(void);
139 static void pane_row_range_display(int r0, int r1);
140 static void pane_status_display(void);
141 static void pane_caret_display(void);
143 static void insert_char(wchar_t c);
144 static void delete_char_before(void);
145 static void delete_char_after(void);
146 static void caret_update(void);
147 static void caret_move_relative(int drow, int dcolumn, enum dir_spec align_dir, bool select);
148 static void caret_move_absolute(int row, int column, enum dir_spec align_dir, bool select);
149 static void caret_move(spt_t spt, bool select, bool update_ideal_column);
150 static void caret_move_word_left(bool select);
151 static void caret_move_word_right(bool select);
152 static void caret_go_to_line_ask(void);
154 static bool selection_active(void);
155 static void selection_sel_all(void);
156 static void selection_sel_range(spt_t pa, spt_t pb);
157 static void selection_get_points(spt_t *pa, spt_t *pb);
158 static void selection_delete(void);
159 static void selection_copy(void);
160 static void insert_clipboard_data(void);
162 static void search(char *pattern, bool reverse);
163 static void search_prompt(bool reverse);
164 static void search_repeat(void);
166 static void pt_get_sof(spt_t *pt);
167 static void pt_get_eof(spt_t *pt);
168 static void pt_get_sol(spt_t *cpt, spt_t *spt);
169 static void pt_get_eol(spt_t *cpt, spt_t *ept);
170 static bool pt_is_word_beginning(spt_t *pt);
171 static bool pt_is_delimiter(spt_t *pt);
172 static bool pt_is_punctuation(spt_t *pt);
173 static spt_t pt_find_word_left(spt_t spt);
174 static spt_t pt_find_word_left(spt_t spt);
176 static int tag_cmp(tag_t const *a, tag_t const *b);
177 static int spt_cmp(spt_t const *a, spt_t const *b);
178 static int coord_cmp(coord_t const *a, coord_t const *b);
180 static void status_display(char const *str);
183 int main(int argc, char *argv[])
185 kbd_event_t ev;
186 bool new_file;
187 int rc;
189 con = console_init(stdin, stdout);
190 console_clear(con);
192 console_get_size(con, &scr_columns, &scr_rows);
194 pane.rows = scr_rows - 1;
195 pane.columns = scr_columns;
196 pane.sh_row = 1;
197 pane.sh_column = 1;
199 /* Start with an empty sheet. */
200 rc = sheet_create(&doc.sh);
201 if (rc != EOK) {
202 printf("Out of memory.\n");
203 return -1;
206 /* Place caret at the beginning of file. */
207 spt_t sof;
208 pt_get_sof(&sof);
209 sheet_place_tag(doc.sh, &sof, &pane.caret_pos);
210 pane.ideal_column = 1;
212 if (argc == 2) {
213 doc.file_name = str_dup(argv[1]);
214 } else if (argc > 1) {
215 printf("Invalid arguments.\n");
216 return -2;
217 } else {
218 doc.file_name = NULL;
221 new_file = false;
223 if (doc.file_name == NULL || file_insert(doc.file_name) != EOK)
224 new_file = true;
226 /* Place selection start tag. */
227 sheet_place_tag(doc.sh, &sof, &pane.sel_start);
229 /* Move to beginning of file. */
230 pt_get_sof(&sof);
231 caret_move(sof, true, true);
233 /* Initial display */
234 cursor_visible = true;
236 cursor_hide();
237 console_clear(con);
238 pane_text_display();
239 pane_status_display();
240 if (new_file && doc.file_name != NULL)
241 status_display("File not found. Starting empty file.");
242 pane_caret_display();
243 cursor_show();
245 done = false;
247 while (!done) {
248 console_get_kbd_event(con, &ev);
249 pane.rflags = 0;
251 if (ev.type == KEY_PRESS) {
252 /* Handle key press. */
253 if (((ev.mods & KM_ALT) == 0) &&
254 ((ev.mods & KM_SHIFT) == 0) &&
255 (ev.mods & KM_CTRL) != 0) {
256 key_handle_ctrl(&ev);
257 } else if (((ev.mods & KM_ALT) == 0) &&
258 ((ev.mods & KM_CTRL) == 0) &&
259 (ev.mods & KM_SHIFT) != 0) {
260 key_handle_shift(&ev);
261 } else if (((ev.mods & KM_ALT) == 0) &&
262 ((ev.mods & KM_CTRL) != 0) &&
263 (ev.mods & KM_SHIFT) != 0) {
264 key_handle_shift_ctrl(&ev);
265 } else if ((ev.mods & (KM_CTRL | KM_ALT | KM_SHIFT)) == 0) {
266 key_handle_unmod(&ev);
270 /* Redraw as necessary. */
272 cursor_hide();
274 if (pane.rflags & REDRAW_TEXT)
275 pane_text_display();
276 if (pane.rflags & REDRAW_ROW)
277 pane_row_display();
278 if (pane.rflags & REDRAW_STATUS)
279 pane_status_display();
280 if (pane.rflags & REDRAW_CARET)
281 pane_caret_display();
283 cursor_show();
286 console_clear(con);
288 return 0;
291 static void cursor_show(void)
293 cursor_setvis(true);
296 static void cursor_hide(void)
298 cursor_setvis(false);
301 static void cursor_setvis(bool visible)
303 if (cursor_visible != visible) {
304 console_cursor_visibility(con, visible);
305 cursor_visible = visible;
309 /** Handle key without modifier. */
310 static void key_handle_unmod(kbd_event_t const *ev)
312 switch (ev->key) {
313 case KC_ENTER:
314 selection_delete();
315 insert_char('\n');
316 caret_update();
317 break;
318 case KC_LEFT:
319 case KC_RIGHT:
320 case KC_UP:
321 case KC_DOWN:
322 case KC_HOME:
323 case KC_END:
324 case KC_PAGE_UP:
325 case KC_PAGE_DOWN:
326 key_handle_movement(ev->key, false);
327 break;
328 case KC_BACKSPACE:
329 if (selection_active())
330 selection_delete();
331 else
332 delete_char_before();
333 caret_update();
334 break;
335 case KC_DELETE:
336 if (selection_active())
337 selection_delete();
338 else
339 delete_char_after();
340 caret_update();
341 break;
342 default:
343 if (ev->c >= 32 || ev->c == '\t') {
344 selection_delete();
345 insert_char(ev->c);
346 caret_update();
348 break;
352 /** Handle Shift-key combination. */
353 static void key_handle_shift(kbd_event_t const *ev)
355 switch (ev->key) {
356 case KC_LEFT:
357 case KC_RIGHT:
358 case KC_UP:
359 case KC_DOWN:
360 case KC_HOME:
361 case KC_END:
362 case KC_PAGE_UP:
363 case KC_PAGE_DOWN:
364 key_handle_movement(ev->key, true);
365 break;
366 default:
367 if (ev->c >= 32 || ev->c == '\t') {
368 selection_delete();
369 insert_char(ev->c);
370 caret_update();
372 break;
376 /** Handle Ctrl-key combination. */
377 static void key_handle_ctrl(kbd_event_t const *ev)
379 spt_t pt;
380 switch (ev->key) {
381 case KC_Q:
382 done = true;
383 break;
384 case KC_S:
385 if (doc.file_name != NULL)
386 file_save(doc.file_name);
387 else
388 file_save_as();
389 break;
390 case KC_E:
391 file_save_as();
392 break;
393 case KC_C:
394 selection_copy();
395 break;
396 case KC_V:
397 selection_delete();
398 insert_clipboard_data();
399 pane.rflags |= REDRAW_TEXT;
400 caret_update();
401 break;
402 case KC_X:
403 selection_copy();
404 selection_delete();
405 pane.rflags |= REDRAW_TEXT;
406 caret_update();
407 break;
408 case KC_A:
409 selection_sel_all();
410 break;
411 case KC_RIGHT:
412 caret_move_word_right(false);
413 break;
414 case KC_LEFT:
415 caret_move_word_left(false);
416 break;
417 case KC_L:
418 caret_go_to_line_ask();
419 break;
420 case KC_F:
421 search_prompt(false);
422 break;
423 case KC_N:
424 search_repeat();
425 break;
426 case KC_HOME:
427 pt_get_sof(&pt);
428 caret_move(pt, false, true);
429 break;
430 case KC_END:
431 pt_get_eof(&pt);
432 caret_move(pt, false, true);
433 break;
434 default:
435 break;
439 static void key_handle_shift_ctrl(kbd_event_t const *ev)
441 spt_t pt;
442 switch(ev->key) {
443 case KC_LEFT:
444 caret_move_word_left(true);
445 break;
446 case KC_RIGHT:
447 caret_move_word_right(true);
448 break;
449 case KC_F:
450 search_prompt(true);
451 break;
452 case KC_HOME:
453 pt_get_sof(&pt);
454 caret_move(pt, true, true);
455 break;
456 case KC_END:
457 pt_get_eof(&pt);
458 caret_move(pt, true, true);
459 break;
460 default:
461 break;
465 /** Move caret while preserving or resetting selection. */
466 static void caret_move(spt_t new_caret_pt, bool select, bool update_ideal_column)
468 spt_t old_caret_pt, old_sel_pt;
469 coord_t c_old, c_new;
470 bool had_sel;
472 /* Check if we had selection before. */
473 tag_get_pt(&pane.caret_pos, &old_caret_pt);
474 tag_get_pt(&pane.sel_start, &old_sel_pt);
475 had_sel = !spt_equal(&old_caret_pt, &old_sel_pt);
477 /* Place tag of the caret */
478 sheet_remove_tag(doc.sh, &pane.caret_pos);
479 sheet_place_tag(doc.sh, &new_caret_pt, &pane.caret_pos);
481 if (select == false) {
482 /* Move sel_start to the same point as caret. */
483 sheet_remove_tag(doc.sh, &pane.sel_start);
484 sheet_place_tag(doc.sh, &new_caret_pt, &pane.sel_start);
487 spt_get_coord(&new_caret_pt, &c_new);
488 if (select) {
489 spt_get_coord(&old_caret_pt, &c_old);
491 if (c_old.row == c_new.row)
492 pane.rflags |= REDRAW_ROW;
493 else
494 pane.rflags |= REDRAW_TEXT;
496 } else if (had_sel == true) {
497 /* Redraw because text was unselected. */
498 pane.rflags |= REDRAW_TEXT;
501 if (update_ideal_column)
502 pane.ideal_column = c_new.column;
504 caret_update();
507 static void key_handle_movement(unsigned int key, bool select)
509 switch (key) {
510 case KC_LEFT:
511 caret_move_relative(0, -1, dir_before, select);
512 break;
513 case KC_RIGHT:
514 caret_move_relative(0, 0, dir_after, select);
515 break;
516 case KC_UP:
517 caret_move_relative(-1, 0, dir_before, select);
518 break;
519 case KC_DOWN:
520 caret_move_relative(+1, 0, dir_before, select);
521 break;
522 case KC_HOME:
523 caret_move_relative(0, -ED_INFTY, dir_after, select);
524 break;
525 case KC_END:
526 caret_move_relative(0, +ED_INFTY, dir_before, select);
527 break;
528 case KC_PAGE_UP:
529 caret_move_relative(-pane.rows, 0, dir_before, select);
530 break;
531 case KC_PAGE_DOWN:
532 caret_move_relative(+pane.rows, 0, dir_before, select);
533 break;
534 default:
535 break;
539 /** Save the document. */
540 static int file_save(char const *fname)
542 spt_t sp, ep;
543 int rc;
545 status_display("Saving...");
546 pt_get_sof(&sp);
547 pt_get_eof(&ep);
549 rc = file_save_range(fname, &sp, &ep);
551 switch (rc) {
552 case EINVAL:
553 status_display("Error opening file!");
554 break;
555 case EIO:
556 status_display("Error writing data!");
557 break;
558 default:
559 status_display("File saved.");
560 break;
563 return rc;
566 /** Change document name and save. */
567 static void file_save_as(void)
569 const char *old_fname = (doc.file_name != NULL) ? doc.file_name : "";
570 char *fname;
572 fname = prompt("Save As", old_fname);
573 if (fname == NULL) {
574 status_display("Save cancelled.");
575 return;
578 int rc = file_save(fname);
579 if (rc != EOK)
580 return;
582 if (doc.file_name != NULL)
583 free(doc.file_name);
584 doc.file_name = fname;
587 /** Ask for a string. */
588 static char *prompt(char const *prompt, char const *init_value)
590 kbd_event_t ev;
591 char *str;
592 wchar_t buffer[INFNAME_MAX_LEN + 1];
593 int max_len;
594 int nc;
595 bool done;
597 asprintf(&str, "%s: %s", prompt, init_value);
598 status_display(str);
599 console_set_pos(con, 1 + str_length(str), scr_rows - 1);
600 free(str);
602 console_set_style(con, STYLE_INVERTED);
604 max_len = min(INFNAME_MAX_LEN, scr_columns - 4 - str_length(prompt));
605 str_to_wstr(buffer, max_len + 1, init_value);
606 nc = wstr_length(buffer);
607 done = false;
609 while (!done) {
610 console_get_kbd_event(con, &ev);
612 if (ev.type == KEY_PRESS) {
613 /* Handle key press. */
614 if (((ev.mods & KM_ALT) == 0) &&
615 (ev.mods & KM_CTRL) != 0) {
617 } else if ((ev.mods & (KM_CTRL | KM_ALT)) == 0) {
618 switch (ev.key) {
619 case KC_ESCAPE:
620 return NULL;
621 case KC_BACKSPACE:
622 if (nc > 0) {
623 putchar('\b');
624 console_flush(con);
625 --nc;
627 break;
628 case KC_ENTER:
629 done = true;
630 break;
631 default:
632 if (ev.c >= 32 && nc < max_len) {
633 putchar(ev.c);
634 console_flush(con);
635 buffer[nc++] = ev.c;
637 break;
643 buffer[nc] = '\0';
644 str = wstr_to_astr(buffer);
646 console_set_style(con, STYLE_NORMAL);
648 return str;
651 /** Insert file at caret position.
653 * Reads in the contents of a file and inserts them at the current position
654 * of the caret.
656 static int file_insert(char *fname)
658 FILE *f;
659 wchar_t c;
660 char buf[BUF_SIZE];
661 int bcnt;
662 int n_read;
663 size_t off;
665 f = fopen(fname, "rt");
666 if (f == NULL)
667 return EINVAL;
669 bcnt = 0;
671 while (true) {
672 if (bcnt < STR_BOUNDS(1)) {
673 n_read = fread(buf + bcnt, 1, BUF_SIZE - bcnt, f);
674 bcnt += n_read;
677 off = 0;
678 c = str_decode(buf, &off, bcnt);
679 if (c == '\0')
680 break;
682 bcnt -= off;
683 memcpy(buf, buf + off, bcnt);
685 insert_char(c);
688 fclose(f);
690 return EOK;
693 /** Save a range of text into a file. */
694 static int file_save_range(char const *fname, spt_t const *spos,
695 spt_t const *epos)
697 FILE *f;
698 char buf[BUF_SIZE];
699 spt_t sp, bep;
700 size_t bytes, n_written;
702 f = fopen(fname, "wt");
703 if (f == NULL)
704 return EINVAL;
706 sp = *spos;
708 do {
709 sheet_copy_out(doc.sh, &sp, epos, buf, BUF_SIZE, &bep);
710 bytes = str_size(buf);
712 n_written = fwrite(buf, 1, bytes, f);
713 if (n_written != bytes) {
714 return EIO;
717 sp = bep;
718 } while (!spt_equal(&bep, epos));
720 if (fclose(f) != EOK)
721 return EIO;
723 return EOK;
726 /** Return contents of range as a new string. */
727 static char *range_get_str(spt_t const *spos, spt_t const *epos)
729 char *buf;
730 spt_t sp, bep;
731 size_t bytes;
732 size_t buf_size, bpos;
734 buf_size = 1;
736 buf = malloc(buf_size);
737 if (buf == NULL)
738 return NULL;
740 bpos = 0;
741 sp = *spos;
743 while (true) {
744 sheet_copy_out(doc.sh, &sp, epos, &buf[bpos], buf_size - bpos,
745 &bep);
746 bytes = str_size(&buf[bpos]);
747 bpos += bytes;
748 sp = bep;
750 if (spt_equal(&bep, epos))
751 break;
753 buf_size *= 2;
754 buf = realloc(buf, buf_size);
755 if (buf == NULL)
756 return NULL;
759 return buf;
762 static void pane_text_display(void)
764 int sh_rows, rows;
766 sheet_get_num_rows(doc.sh, &sh_rows);
767 rows = min(sh_rows - pane.sh_row + 1, pane.rows);
769 /* Draw rows from the sheet. */
771 console_set_pos(con, 0, 0);
772 pane_row_range_display(0, rows);
774 /* Clear the remaining rows if file is short. */
776 int i;
777 sysarg_t j;
778 for (i = rows; i < pane.rows; ++i) {
779 console_set_pos(con, 0, i);
780 for (j = 0; j < scr_columns; ++j)
781 putchar(' ');
782 console_flush(con);
785 pane.rflags |= (REDRAW_STATUS | REDRAW_CARET);
786 pane.rflags &= ~REDRAW_ROW;
789 /** Display just the row where the caret is. */
790 static void pane_row_display(void)
792 spt_t caret_pt;
793 coord_t coord;
794 int ridx;
796 tag_get_pt(&pane.caret_pos, &caret_pt);
797 spt_get_coord(&caret_pt, &coord);
799 ridx = coord.row - pane.sh_row;
800 pane_row_range_display(ridx, ridx + 1);
801 pane.rflags |= (REDRAW_STATUS | REDRAW_CARET);
804 static void pane_row_range_display(int r0, int r1)
806 int i, j, fill;
807 spt_t rb, re, dep, pt;
808 coord_t rbc, rec;
809 char row_buf[ROW_BUF_SIZE];
810 wchar_t c;
811 size_t pos, size;
812 int s_column;
813 coord_t csel_start, csel_end, ctmp;
815 /* Determine selection start and end. */
817 tag_get_pt(&pane.sel_start, &pt);
818 spt_get_coord(&pt, &csel_start);
820 tag_get_pt(&pane.caret_pos, &pt);
821 spt_get_coord(&pt, &csel_end);
823 if (coord_cmp(&csel_start, &csel_end) > 0) {
824 ctmp = csel_start;
825 csel_start = csel_end;
826 csel_end = ctmp;
829 /* Draw rows from the sheet. */
831 console_set_pos(con, 0, 0);
832 for (i = r0; i < r1; ++i) {
833 /* Starting point for row display */
834 rbc.row = pane.sh_row + i;
835 rbc.column = pane.sh_column;
836 sheet_get_cell_pt(doc.sh, &rbc, dir_before, &rb);
838 /* Ending point for row display */
839 rec.row = pane.sh_row + i;
840 rec.column = pane.sh_column + pane.columns;
841 sheet_get_cell_pt(doc.sh, &rec, dir_before, &re);
843 /* Copy the text of the row to the buffer. */
844 sheet_copy_out(doc.sh, &rb, &re, row_buf, ROW_BUF_SIZE, &dep);
846 /* Display text from the buffer. */
848 if (coord_cmp(&csel_start, &rbc) <= 0 &&
849 coord_cmp(&rbc, &csel_end) < 0) {
850 console_flush(con);
851 console_set_style(con, STYLE_SELECTED);
852 console_flush(con);
855 console_set_pos(con, 0, i);
856 size = str_size(row_buf);
857 pos = 0;
858 s_column = pane.sh_column;
859 while (pos < size) {
860 if ((csel_start.row == rbc.row) && (csel_start.column == s_column)) {
861 console_flush(con);
862 console_set_style(con, STYLE_SELECTED);
863 console_flush(con);
866 if ((csel_end.row == rbc.row) && (csel_end.column == s_column)) {
867 console_flush(con);
868 console_set_style(con, STYLE_NORMAL);
869 console_flush(con);
872 c = str_decode(row_buf, &pos, size);
873 if (c != '\t') {
874 printf("%lc", (wint_t) c);
875 s_column += 1;
876 } else {
877 fill = 1 + ALIGN_UP(s_column, TAB_WIDTH)
878 - s_column;
880 for (j = 0; j < fill; ++j)
881 putchar(' ');
882 s_column += fill;
886 if ((csel_end.row == rbc.row) && (csel_end.column == s_column)) {
887 console_flush(con);
888 console_set_style(con, STYLE_NORMAL);
889 console_flush(con);
892 /* Fill until the end of display area. */
894 if ((unsigned)s_column - 1 < scr_columns)
895 fill = scr_columns - (s_column - 1);
896 else
897 fill = 0;
899 for (j = 0; j < fill; ++j)
900 putchar(' ');
901 console_flush(con);
902 console_set_style(con, STYLE_NORMAL);
905 pane.rflags |= REDRAW_CARET;
908 /** Display pane status in the status line. */
909 static void pane_status_display(void)
911 spt_t caret_pt;
912 coord_t coord;
913 int last_row;
915 tag_get_pt(&pane.caret_pos, &caret_pt);
916 spt_get_coord(&caret_pt, &coord);
918 sheet_get_num_rows(doc.sh, &last_row);
920 const char *fname = (doc.file_name != NULL) ? doc.file_name : "<unnamed>";
922 console_set_pos(con, 0, scr_rows - 1);
923 console_set_style(con, STYLE_INVERTED);
924 int n = printf(" %d, %d (%d): File '%s'. Ctrl-Q Quit Ctrl-S Save "
925 "Ctrl-E Save As", coord.row, coord.column, last_row, fname);
927 int pos = scr_columns - 1 - n;
928 printf("%*s", pos, "");
929 console_flush(con);
930 console_set_style(con, STYLE_NORMAL);
932 pane.rflags |= REDRAW_CARET;
935 /** Set cursor to reflect position of the caret. */
936 static void pane_caret_display(void)
938 spt_t caret_pt;
939 coord_t coord;
941 tag_get_pt(&pane.caret_pos, &caret_pt);
943 spt_get_coord(&caret_pt, &coord);
944 console_set_pos(con, coord.column - pane.sh_column,
945 coord.row - pane.sh_row);
948 /** Insert a character at caret position. */
949 static void insert_char(wchar_t c)
951 spt_t pt;
952 char cbuf[STR_BOUNDS(1) + 1];
953 size_t offs;
955 tag_get_pt(&pane.caret_pos, &pt);
957 offs = 0;
958 chr_encode(c, cbuf, &offs, STR_BOUNDS(1) + 1);
959 cbuf[offs] = '\0';
961 (void) sheet_insert(doc.sh, &pt, dir_before, cbuf);
963 pane.rflags |= REDRAW_ROW;
964 if (c == '\n')
965 pane.rflags |= REDRAW_TEXT;
968 /** Delete the character before the caret. */
969 static void delete_char_before(void)
971 spt_t sp, ep;
972 coord_t coord;
974 tag_get_pt(&pane.caret_pos, &ep);
975 spt_get_coord(&ep, &coord);
977 coord.column -= 1;
978 sheet_get_cell_pt(doc.sh, &coord, dir_before, &sp);
980 (void) sheet_delete(doc.sh, &sp, &ep);
982 pane.rflags |= REDRAW_ROW;
983 if (coord.column < 1)
984 pane.rflags |= REDRAW_TEXT;
987 /** Delete the character after the caret. */
988 static void delete_char_after(void)
990 spt_t sp, ep;
991 coord_t sc, ec;
993 tag_get_pt(&pane.caret_pos, &sp);
994 spt_get_coord(&sp, &sc);
996 sheet_get_cell_pt(doc.sh, &sc, dir_after, &ep);
997 spt_get_coord(&ep, &ec);
999 (void) sheet_delete(doc.sh, &sp, &ep);
1001 pane.rflags |= REDRAW_ROW;
1002 if (ec.row != sc.row)
1003 pane.rflags |= REDRAW_TEXT;
1006 /** Scroll pane after caret has moved.
1008 * After modifying the position of the caret, this is called to scroll
1009 * the pane to ensure that the caret is in the visible area.
1011 static void caret_update(void)
1013 spt_t pt;
1014 coord_t coord;
1016 tag_get_pt(&pane.caret_pos, &pt);
1017 spt_get_coord(&pt, &coord);
1019 /* Scroll pane vertically. */
1021 if (coord.row < pane.sh_row) {
1022 pane.sh_row = coord.row;
1023 pane.rflags |= REDRAW_TEXT;
1026 if (coord.row > pane.sh_row + pane.rows - 1) {
1027 pane.sh_row = coord.row - pane.rows + 1;
1028 pane.rflags |= REDRAW_TEXT;
1031 /* Scroll pane horizontally. */
1033 if (coord.column < pane.sh_column) {
1034 pane.sh_column = coord.column;
1035 pane.rflags |= REDRAW_TEXT;
1038 if (coord.column > pane.sh_column + pane.columns - 1) {
1039 pane.sh_column = coord.column - pane.columns + 1;
1040 pane.rflags |= REDRAW_TEXT;
1043 pane.rflags |= (REDRAW_CARET | REDRAW_STATUS);
1046 /** Relatively move caret position.
1048 * Moves caret relatively to the current position. Looking at the first
1049 * character cell after the caret and moving by @a drow and @a dcolumn, we get
1050 * to a new character cell, and thus a new character. Then we either go to the
1051 * point before the the character or after it, depending on @a align_dir.
1053 * @param select true if the selection tag should stay where it is
1055 static void caret_move_relative(int drow, int dcolumn, enum dir_spec align_dir,
1056 bool select)
1058 spt_t pt;
1059 coord_t coord;
1060 int num_rows;
1061 bool pure_vertical;
1063 tag_get_pt(&pane.caret_pos, &pt);
1064 spt_get_coord(&pt, &coord);
1065 coord.row += drow; coord.column += dcolumn;
1067 /* Clamp coordinates. */
1068 if (drow < 0 && coord.row < 1) coord.row = 1;
1069 if (dcolumn < 0 && coord.column < 1) {
1070 if (coord.row < 2)
1071 coord.column = 1;
1072 else {
1073 coord.row--;
1074 sheet_get_row_width(doc.sh, coord.row, &coord.column);
1077 if (drow > 0) {
1078 sheet_get_num_rows(doc.sh, &num_rows);
1079 if (coord.row > num_rows) coord.row = num_rows;
1082 /* For purely vertical movement try attaining @c ideal_column. */
1083 pure_vertical = (dcolumn == 0 && align_dir == dir_before);
1084 if (pure_vertical)
1085 coord.column = pane.ideal_column;
1088 * Select the point before or after the character at the designated
1089 * coordinates. The character can be wider than one cell (e.g. tab).
1091 sheet_get_cell_pt(doc.sh, &coord, align_dir, &pt);
1093 /* For non-vertical movement set the new value for @c ideal_column. */
1094 caret_move(pt, select, !pure_vertical);
1097 /** Absolutely move caret position.
1099 * Moves caret to a specified position. We get to a new character cell, and
1100 * thus a new character. Then we either go to the point before the the character
1101 * or after it, depending on @a align_dir.
1103 * @param select true if the selection tag should stay where it is
1105 static void caret_move_absolute(int row, int column, enum dir_spec align_dir,
1106 bool select)
1108 coord_t coord;
1109 coord.row = row;
1110 coord.column = column;
1112 spt_t pt;
1113 sheet_get_cell_pt(doc.sh, &coord, align_dir, &pt);
1115 caret_move(pt, select, true);
1118 /** Find beginning of a word to the left of spt */
1119 static spt_t pt_find_word_left(spt_t spt)
1121 do {
1122 spt_prev_char(spt, &spt);
1123 } while (!pt_is_word_beginning(&spt));
1124 return spt;
1127 /** Find beginning of a word to the right of spt */
1128 static spt_t pt_find_word_right(spt_t spt)
1130 do {
1131 spt_next_char(spt, &spt);
1132 } while (!pt_is_word_beginning(&spt));
1133 return spt;
1136 static void caret_move_word_left(bool select)
1138 spt_t pt;
1139 tag_get_pt(&pane.caret_pos, &pt);
1140 spt_t word_left = pt_find_word_left(pt);
1141 caret_move(word_left, select, true);
1144 static void caret_move_word_right(bool select)
1146 spt_t pt;
1147 tag_get_pt(&pane.caret_pos, &pt);
1148 spt_t word_right = pt_find_word_right(pt);
1149 caret_move(word_right, select, true);
1152 /** Ask for line and go to it. */
1153 static void caret_go_to_line_ask(void)
1155 char *sline;
1157 sline = prompt("Go to line", "");
1158 if (sline == NULL) {
1159 status_display("Go to line cancelled.");
1160 return;
1163 char *endptr;
1164 int line = strtol(sline, &endptr, 10);
1165 if (*endptr != '\0') {
1166 free(sline);
1167 status_display("Invalid number entered.");
1168 return;
1170 free(sline);
1172 caret_move_absolute(line, pane.ideal_column, dir_before, false);
1175 /* Search operations */
1176 static int search_spt_producer(void *data, wchar_t *ret)
1178 assert(data != NULL);
1179 assert(ret != NULL);
1180 spt_t *spt = data;
1181 *ret = spt_next_char(*spt, spt);
1182 return EOK;
1185 static int search_spt_reverse_producer(void *data, wchar_t *ret)
1187 assert(data != NULL);
1188 assert(ret != NULL);
1189 spt_t *spt = data;
1190 *ret = spt_prev_char(*spt, spt);
1191 return EOK;
1194 static int search_spt_mark(void *data, void **mark)
1196 assert(data != NULL);
1197 assert(mark != NULL);
1198 spt_t *spt = data;
1199 spt_t *new = calloc(1, sizeof(spt_t));
1200 *mark = new;
1201 if (new == NULL)
1202 return ENOMEM;
1203 *new = *spt;
1204 return EOK;
1207 static void search_spt_mark_free(void *data)
1209 free(data);
1212 static search_ops_t search_spt_ops = {
1213 .equals = char_exact_equals,
1214 .producer = search_spt_producer,
1215 .mark = search_spt_mark,
1216 .mark_free = search_spt_mark_free,
1219 static search_ops_t search_spt_reverse_ops = {
1220 .equals = char_exact_equals,
1221 .producer = search_spt_reverse_producer,
1222 .mark = search_spt_mark,
1223 .mark_free = search_spt_mark_free,
1226 /** Ask for line and go to it. */
1227 static void search_prompt(bool reverse)
1229 char *pattern;
1231 const char *prompt_text = "Find next";
1232 if (reverse)
1233 prompt_text = "Find previous";
1235 const char *default_value = "";
1236 if (pane.previous_search)
1237 default_value = pane.previous_search;
1239 pattern = prompt(prompt_text, default_value);
1240 if (pattern == NULL) {
1241 status_display("Search cancelled.");
1242 return;
1245 if (pane.previous_search)
1246 free(pane.previous_search);
1247 pane.previous_search = pattern;
1248 pane.previous_search_reverse = reverse;
1250 search(pattern, reverse);
1253 static void search_repeat(void)
1255 if (pane.previous_search == NULL) {
1256 status_display("No previous search to repeat.");
1257 return;
1260 search(pane.previous_search, pane.previous_search_reverse);
1263 static void search(char *pattern, bool reverse)
1265 status_display("Searching...");
1267 spt_t sp, producer_pos;
1268 tag_get_pt(&pane.caret_pos, &sp);
1270 /* Start searching on the position before/after caret */
1271 if (!reverse) {
1272 spt_next_char(sp, &sp);
1274 else {
1275 spt_prev_char(sp, &sp);
1277 producer_pos = sp;
1279 search_ops_t ops = search_spt_ops;
1280 if (reverse)
1281 ops = search_spt_reverse_ops;
1283 search_t *search = search_init(pattern, &producer_pos, ops, reverse);
1284 if (search == NULL) {
1285 status_display("Failed initializing search.");
1286 return;
1289 match_t match;
1290 int rc = search_next_match(search, &match);
1291 if (rc != EOK) {
1292 status_display("Failed searching.");
1293 search_fini(search);
1296 if (match.end) {
1297 status_display("Match found.");
1298 assert(match.end != NULL);
1299 spt_t *end = match.end;
1300 caret_move(*end, false, true);
1301 while (match.length > 0) {
1302 match.length--;
1303 if (reverse) {
1304 spt_next_char(*end, end);
1306 else {
1307 spt_prev_char(*end, end);
1310 caret_move(*end, true, true);
1311 free(end);
1313 else {
1314 status_display("Not found.");
1317 search_fini(search);
1320 /** Check for non-empty selection. */
1321 static bool selection_active(void)
1323 return (tag_cmp(&pane.caret_pos, &pane.sel_start) != 0);
1326 static void selection_get_points(spt_t *pa, spt_t *pb)
1328 spt_t pt;
1330 tag_get_pt(&pane.sel_start, pa);
1331 tag_get_pt(&pane.caret_pos, pb);
1333 if (spt_cmp(pa, pb) > 0) {
1334 pt = *pa;
1335 *pa = *pb;
1336 *pb = pt;
1340 /** Delete selected text. */
1341 static void selection_delete(void)
1343 spt_t pa, pb;
1344 coord_t ca, cb;
1345 int rel;
1347 tag_get_pt(&pane.sel_start, &pa);
1348 tag_get_pt(&pane.caret_pos, &pb);
1349 spt_get_coord(&pa, &ca);
1350 spt_get_coord(&pb, &cb);
1351 rel = coord_cmp(&ca, &cb);
1353 if (rel == 0)
1354 return;
1356 if (rel < 0)
1357 sheet_delete(doc.sh, &pa, &pb);
1358 else
1359 sheet_delete(doc.sh, &pb, &pa);
1361 if (ca.row == cb.row)
1362 pane.rflags |= REDRAW_ROW;
1363 else
1364 pane.rflags |= REDRAW_TEXT;
1367 /** Select all text in the editor */
1368 static void selection_sel_all(void)
1370 spt_t spt, ept;
1372 pt_get_sof(&spt);
1373 pt_get_eof(&ept);
1375 selection_sel_range(spt, ept);
1378 /** Select select all text in a given range with the given direction */
1379 static void selection_sel_range(spt_t pa, spt_t pb)
1381 sheet_remove_tag(doc.sh, &pane.sel_start);
1382 sheet_place_tag(doc.sh, &pa, &pane.sel_start);
1383 sheet_remove_tag(doc.sh, &pane.caret_pos);
1384 sheet_place_tag(doc.sh, &pb, &pane.caret_pos);
1386 pane.rflags |= REDRAW_TEXT;
1387 caret_update();
1390 static void selection_copy(void)
1392 spt_t pa, pb;
1393 char *str;
1395 selection_get_points(&pa, &pb);
1396 str = range_get_str(&pa, &pb);
1397 if (str == NULL || clipboard_put_str(str) != EOK) {
1398 status_display("Copying to clipboard failed!");
1400 free(str);
1403 static void insert_clipboard_data(void)
1405 char *str;
1406 size_t off;
1407 wchar_t c;
1408 int rc;
1410 rc = clipboard_get_str(&str);
1411 if (rc != EOK || str == NULL)
1412 return;
1414 off = 0;
1416 while (true) {
1417 c = str_decode(str, &off, STR_NO_LIMIT);
1418 if (c == '\0')
1419 break;
1421 insert_char(c);
1424 free(str);
1427 /** Get start-of-file s-point. */
1428 static void pt_get_sof(spt_t *pt)
1430 coord_t coord;
1432 coord.row = coord.column = 1;
1433 sheet_get_cell_pt(doc.sh, &coord, dir_before, pt);
1436 /** Get end-of-file s-point. */
1437 static void pt_get_eof(spt_t *pt)
1439 coord_t coord;
1440 int num_rows;
1442 sheet_get_num_rows(doc.sh, &num_rows);
1443 coord.row = num_rows + 1;
1444 coord.column = 1;
1446 sheet_get_cell_pt(doc.sh, &coord, dir_after, pt);
1449 /** Get start-of-line s-point for given s-point cpt */
1450 static void pt_get_sol(spt_t *cpt, spt_t *spt)
1452 coord_t coord;
1454 spt_get_coord(cpt, &coord);
1455 coord.column = 1;
1457 sheet_get_cell_pt(doc.sh, &coord, dir_before, spt);
1460 /** Get end-of-line s-point for given s-point cpt */
1461 static void pt_get_eol(spt_t *cpt, spt_t *ept)
1463 coord_t coord;
1464 int row_width;
1466 spt_get_coord(cpt, &coord);
1467 sheet_get_row_width(doc.sh, coord.row, &row_width);
1468 coord.column = row_width - 1;
1470 sheet_get_cell_pt(doc.sh, &coord, dir_after, ept);
1473 /** Check whether the spt is at a beginning of a word */
1474 static bool pt_is_word_beginning(spt_t *pt)
1476 spt_t lp, sfp, efp, slp, elp;
1477 coord_t coord;
1479 pt_get_sof(&sfp);
1480 pt_get_eof(&efp);
1481 pt_get_sol(pt, &slp);
1482 pt_get_eol(pt, &elp);
1484 /* the spt is at the beginning or end of the file or line */
1485 if ((spt_cmp(&sfp, pt) == 0) || (spt_cmp(&efp, pt) == 0)
1486 || (spt_cmp(&slp, pt) == 0) || (spt_cmp(&elp, pt) == 0))
1487 return true;
1489 /* the spt is a delimiter */
1490 if (pt_is_delimiter(pt))
1491 return false;
1493 spt_get_coord(pt, &coord);
1495 coord.column -= 1;
1496 sheet_get_cell_pt(doc.sh, &coord, dir_before, &lp);
1498 return pt_is_delimiter(&lp)
1499 || (pt_is_punctuation(pt) && !pt_is_punctuation(&lp))
1500 || (pt_is_punctuation(&lp) && !pt_is_punctuation(pt));
1503 static wchar_t get_first_wchar(const char *str)
1505 size_t offset = 0;
1506 return str_decode(str, &offset, str_size(str));
1509 static bool pt_is_delimiter(spt_t *pt)
1511 spt_t rp;
1512 coord_t coord;
1513 char *ch = NULL;
1515 spt_get_coord(pt, &coord);
1517 coord.column += 1;
1518 sheet_get_cell_pt(doc.sh, &coord, dir_after, &rp);
1520 ch = range_get_str(pt, &rp);
1521 if (ch == NULL)
1522 return false;
1524 wchar_t first_char = get_first_wchar(ch);
1525 switch(first_char) {
1526 case ' ':
1527 case '\t':
1528 case '\n':
1529 return true;
1530 default:
1531 return false;
1535 static bool pt_is_punctuation(spt_t *pt)
1537 spt_t rp;
1538 coord_t coord;
1539 char *ch = NULL;
1541 spt_get_coord(pt, &coord);
1543 coord.column += 1;
1544 sheet_get_cell_pt(doc.sh, &coord, dir_after, &rp);
1546 ch = range_get_str(pt, &rp);
1547 if (ch == NULL)
1548 return false;
1550 wchar_t first_char = get_first_wchar(ch);
1551 switch(first_char) {
1552 case ',':
1553 case '.':
1554 case ';':
1555 case ':':
1556 case '/':
1557 case '?':
1558 case '\\':
1559 case '|':
1560 case '_':
1561 case '+':
1562 case '-':
1563 case '*':
1564 case '=':
1565 case '<':
1566 case '>':
1567 return true;
1568 default:
1569 return false;
1573 /** Compare tags. */
1574 static int tag_cmp(tag_t const *a, tag_t const *b)
1576 spt_t pa, pb;
1578 tag_get_pt(a, &pa);
1579 tag_get_pt(b, &pb);
1581 return spt_cmp(&pa, &pb);
1584 /** Compare s-points. */
1585 static int spt_cmp(spt_t const *a, spt_t const *b)
1587 coord_t ca, cb;
1589 spt_get_coord(a, &ca);
1590 spt_get_coord(b, &cb);
1592 return coord_cmp(&ca, &cb);
1595 /** Compare coordinats. */
1596 static int coord_cmp(coord_t const *a, coord_t const *b)
1598 if (a->row - b->row != 0)
1599 return a->row - b->row;
1601 return a->column - b->column;
1604 /** Display text in the status line. */
1605 static void status_display(char const *str)
1607 console_set_pos(con, 0, scr_rows - 1);
1608 console_set_style(con, STYLE_INVERTED);
1610 int pos = -(scr_columns - 3);
1611 printf(" %*s ", pos, str);
1612 console_flush(con);
1613 console_set_style(con, STYLE_NORMAL);
1615 pane.rflags |= REDRAW_CARET;
1618 /** @}