mailedit: semi-working autowrapping
[chiroptera.git] / editor.d
blob3a26769231d140aead2d75f42c4abb652419f4cb
1 /* E-Mail Client
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 module editor is aliced;
21 import arsd.simpledisplay;
23 import iv.cmdcon;
24 import iv.strex;
25 import iv.utfutil;
26 import iv.vfs;
28 import egfx;
29 import egui;
30 import egui_dialogs;
33 // ////////////////////////////////////////////////////////////////////////// //
34 // very simple wrapping text editor, without much features
35 class Editor {
36 private:
37 int linewrap = 72; // "normal" line length
38 int maxlinelen = 79; // maximum line length
40 string[] lines;
42 int cx, cy;
43 int markx, marky = -1;
45 public:
46 enum SpecCh : dchar {
47 Left = '\x01',
48 Right = '\x02',
49 Up = '\x03',
50 Down = '\x04',
51 Home = '\x05',
52 End = '\x06',
53 PageUp = '\x07',
54 Backspace = '\x08',
55 Tab = '\x09',
56 Enter = '\x0a',
57 PageDown = '\x0b',
58 KillLine = '\x0c',
59 PutMark = '\x0e',
60 ResetMark = '\x0f',
61 WordLeft = '\x10',
62 WordRight = '\x11',
63 Delete = cast(dchar)127,
66 private:
67 @property bool hasMark () const pure nothrow @safe @nogc { return (marky >= 0); }
69 int xy2pos (int x, int y) const pure nothrow @safe @nogc {
70 if (y < 0) return 0;
71 if (y >= lines.length) y = cast(int)lines.length;
72 uint pos = 0;
73 foreach (immutable yy; 0..y-1) pos += linelen(yy)+1; // 1 for virtual EOL
74 if (x > 0 && y < lines.length) {
75 Utf8DecoderFast dc;
76 string s = lines[y];
77 while (s.length) {
78 if (dc.decode(cast(ubyte)s[0])) {
79 ++pos;
80 if (--x == 0) break;
82 s = s[1..$];
85 return pos;
88 public:
89 this () {
92 void addLine (const(char)[] s) {
93 lines ~= s.idup;
96 @property int lineCount () const pure nothrow @safe @nogc { return cast(int)lines.length; }
98 @property int curx () const pure nothrow @safe @nogc { return cx; }
99 @property int cury () const pure nothrow @safe @nogc { return cy; }
101 void normCX () pure nothrow @safe @nogc { if (cx > 0) { auto llen = linelen(cy); if (cx > llen) cx = llen; } }
103 // empty string: not an attach
104 string getAttachName (int ly) const pure nothrow @trusted @nogc {
105 if (ly < 0 || ly >= lines.length) return null;
106 string s = lines.ptr[ly];
107 if (s.length < 3 || s.ptr[0] != '@' || s.ptr[1] != '@') return null;
108 s = s[2..$].xstrip;
109 if (s.length == 0) return null;
110 // ok, it looks like attach, check next lines
111 foreach (string t; lines[ly..$]) {
112 if (t.length && (t.length < 2 || t.ptr[0] != '@' || t.ptr[0] != '@')) return null;
114 return s;
117 void resetMark () pure nothrow @safe @nogc { markx = 0; marky = -1; }
119 bool isMarked (int x, int y) const pure nothrow @safe @nogc {
120 if (!hasMark) return false;
121 if (cy < marky) {
122 if (y < cy || y > marky) return false;
123 if (y > cy && y < marky) return true;
124 if (y == cy) return (x >= cx);
125 if (y == marky) return (x < markx);
126 assert(0, "wtf?!");
127 } else if (cy > marky) {
128 if (y < marky || y > cy) return false;
129 if (y > marky && y < cy) return true;
130 if (y == marky) return (x >= markx);
131 if (y == cy) return (x < cx);
132 assert(0, "wtf?!");
133 } else {
134 if (y != marky) return false;
135 if (cx == markx) return false;
136 return (cx < markx ? (x >= cx && x < markx) : (x >= markx && x < cx));
138 assert(0, "wtf?!");
141 bool lineHasMark (int y) const pure nothrow @safe @nogc {
142 if (!hasMark) return false;
143 if (cy == marky) return (y == cy);
144 return (cy < marky ? (y >= cy && y <= marky) : (y >= marky && y <= cy));
147 string getSelectionText () const pure nothrow @safe {
148 if (!hasMark || (cx == markx && cy == marky)) return null;
149 int sx, sy, ex, ey;
150 if (cy < marky) { sx = cx; sy = cy; ex = markx; ey = marky; }
151 else if (cy > marky) { sx = markx; sy = marky; ex = cx; ey = cy; }
152 else if (cx < markx) { sx = cx; sy = cy; ex = markx; ey = marky; }
153 else { sx = markx; sy = marky; ex = cx; ey = cy; }
154 string res;
155 while (sx != ex || sy != ey) {
156 dchar ch = chatAt(sx, sy);
157 if (ch == 0) {
158 res ~= "\n";
159 sx = 0;
160 ++sy;
161 } else {
162 res ~= ch;
163 ++sx;
166 return res;
169 // return # of chars taken by quoting
170 int quoteLength (int lidx) const pure nothrow @safe @nogc {
171 if (lidx < 0 || lidx >= lines.length) return 0;
172 string s = lines[lidx];
173 if (s.length == 0 || s[0] != '>') return 0;
174 int pos = 0;
175 while (pos < s.length) {
176 if (s[pos] == ' ') { ++pos; continue; }
177 if (s[pos] != '>') break;
178 ++pos;
180 if (pos < s.length && s[pos] == ' ') ++pos;
181 return pos;
184 int quoteLevel (int lidx) const pure nothrow @safe @nogc {
185 if (lidx < 0 || lidx >= lines.length) return 0;
186 string s = lines[lidx];
187 if (s.length == 0 || s[0] != '>') return 0;
188 int res = 0;
189 while (s.length) {
190 if (s[0] == ' ') { s = s[1..$]; continue; }
191 if (s[0] != '>') break;
192 ++res;
193 s = s[1..$];
195 return res;
198 string opIndex (int idx) const pure nothrow @safe @nogc { return (idx >= 0 && idx < lines.length ? lines[idx] : null); }
200 int linelen (int idx) const pure nothrow @safe @nogc {
201 if (idx < 0 || idx >= lines.length) return 0;
202 string s = lines[idx];
203 if (s.length == 0) return 0;
204 Utf8DecoderFast dc;
205 int res = 0;
206 while (s.length) {
207 if (dc.decode(cast(ubyte)s[0])) ++res;
208 s = s[1..$];
210 return res;
213 // x position to line offset
214 int lineofs (int x, int idx) const pure nothrow @safe @nogc {
215 if (x <= 0 || idx < 0 || idx >= lines.length) return 0;
216 string s = lines[idx];
217 if (x >= s.length) return cast(int)s.length;
218 Utf8DecoderFast dc;
219 int res = 0;
220 while (s.length) {
221 ++res;
222 if (dc.decode(cast(ubyte)s[0])) {
223 if (--x == 0) break;
225 s = s[1..$];
227 return res;
230 dchar chatAt (int x, int y) const pure nothrow @safe @nogc {
231 if (x < 0 || y < 0 || y >= lines.length) return 0;
232 string s = lines[y];
233 if (s.length == 0 || x >= s.length) return 0;
234 Utf8DecoderFast dc;
235 while (s.length) {
236 if (dc.decode(cast(ubyte)s[0])) {
237 if (x-- == 0) return dc.codepoint;
239 s = s[1..$];
241 return 0;
244 // reformat whole text
245 void reformat () {
246 cx = 0;
247 cy = 0;
249 while (cy < lines.length) {
250 if (linelen(cy) > maxlinelen) {
251 int pos = linewrap-1;
252 while (pos >= 0 && chatAt(pos, cy) > ' ') --pos;
253 if (pos > 0) {
254 cx = ++pos;
255 doEnter();
256 } else {
257 ++cy;
260 // join and rewrap
261 if (quoteLevel(cy) != quoteLevel(cy+1)) { ++cy; continue; }
262 if (lines[cy].length == 0 || lines[cy][$-1] > ' ') { ++cy; continue; }
263 doLineJoin();
266 cx = 0;
267 cy = cast(int)lines.length;
270 void putUtf (const(char)[] s) {
271 Utf8DecoderFast dc;
272 foreach (char ch; s) {
273 if (dc.decode(cast(ubyte)ch)) {
274 if (dc.isValidDC(dc.codepoint)) putChar(dc.codepoint); else putChar('?');
279 void putChar (dchar ch) {
280 switch (ch) {
281 case SpecCh.Left: doLeft(); return;
282 case SpecCh.Right: doRight(); return;
283 case SpecCh.Up: doUp(); return;
284 case SpecCh.Down: doDown(); return;
285 case SpecCh.Home: doHome(); return;
286 case SpecCh.End: doEnd(); return;
287 case SpecCh.KillLine:
288 resetMark();
289 if (cy < lines.length) {
290 foreach (immutable c; cy+1..lines.length) lines[c-1] = lines[c];
291 lines.length -= 1;
292 lines.assumeSafeAppend;
293 cx = 0;
295 return;
296 case SpecCh.PutMark: markx = cx; marky = cy; return;
297 case SpecCh.ResetMark: resetMark(); return;
298 case 13: case 10: resetMark(); doEnter!false(); return;
299 case SpecCh.Backspace: resetMark(); doBackspace(); return;
300 case SpecCh.Delete: resetMark(); doDelete(); return;
301 case SpecCh.WordLeft:
302 normCX();
303 doLeft();
304 bool inWord = (chatAt(cx, cy) > ' ');
305 while (cx != 0 || cy != 0) {
306 if (inWord) {
307 if (chatAt(cx, cy) <= ' ') { doRight(); break; }
308 doLeft();
309 } else {
310 if (chatAt(cx, cy) <= ' ') doLeft(); else inWord = true;
313 break;
314 case SpecCh.WordRight:
315 normCX();
316 bool inWord = (chatAt(cx, cy) > ' ');
317 for (;;) {
318 if (cy >= lines.length) break;
319 if (cy == lines.length-1 && cx >= linelen(cy)) break;
320 if (inWord) {
321 if (chatAt(cx, cy) <= ' ') break;
322 doRight();
323 } else {
324 if (chatAt(cx, cy) <= ' ') doRight(); else inWord = true;
327 break;
328 default:
330 if (ch < ' ' || ch == 127) return;
331 resetMark();
332 string s;
333 if (cy >= lines.length) {
334 cx = 0;
335 cy = cast(int)lines.length;
336 s ~= ch;
337 lines ~= s;
338 } else {
339 int len = linelen(cy);
340 if (cx >= len) {
341 cx = len;
342 lines[cy] ~= ch;
343 } else {
344 s = lines[cy];
345 int ofs = lineofs(cx, cy);
346 if (ofs >= s.length) {
347 s ~= ch;
348 } else {
349 string t = s[0..ofs];
350 t ~= ch;
351 s = t~s[ofs..$];
352 lines[cy] = s;
356 doRight();
357 // wrapping (don't wrap attach lines)
358 if (linelen(cy) > linewrap && lines[cy][0..2] != "@@") {
359 int ocx = cx, ocy = cy;
360 while (cy < lines.length) {
361 if (linelen(cy) == 0) break; // stop right here, you criminal scum!
362 if (linelen(cy) > linewrap) {
363 int pos = linewrap-1;
364 while (pos >= 0 && chatAt(pos, cy) > ' ') --pos;
365 if (pos <= 0) break;
366 cx = ++pos;
367 doEnter();
368 } else {
369 if (lines.length-cy < 2) break;
370 // join and rewrap
371 if (quoteLevel(cy) != quoteLevel(cy+1)) break;
372 if (lines[cy].length == 0 || lines[cy][$-1] > ' ') break;
373 if (lines[cy+1].length == 0) break;
374 doLineJoin();
377 // fix cursor coordinates
378 cx = ocx;
379 cy = ocy;
380 while (cy < lines.length) {
381 if (cy == ocy) {
382 if (cx < linelen(cy)) break;
383 } else {
384 if (cx <= linelen(cy)) break;
386 cx -= linelen(cy);
387 ++cy;
388 skipQuotes();
393 private void skipQuotes () {
394 if (cy < 0 || cy >= lines.length) return;
395 string s = lines[cy];
396 if (s.length == 0 || s[0] != '>') return;
397 int pos = 0;
398 while (pos < s.length) {
399 if (s[pos] == ' ' || s[pos] == '>') {
400 ++cx;
401 ++pos;
402 } else {
403 break;
406 if (pos < s.length && s[pos] == ' ') ++cx;
409 void doLineJoin () {
410 if (cy < 0) cy = 0;
411 if (cy >= lines.length || lines.length-cy < 2) return;
412 cx = linelen(cy);
413 string s = lines[cy+1];
414 usize stpos = 0;
415 if (s.length > 0 && s[0] == '>') {
416 while (stpos < s.length) if (s[stpos] == ' ' || s[stpos] == '>') ++stpos; else break;
417 while (stpos < s.length && s[stpos] <= ' ') ++stpos;
418 s = s[stpos..$];
419 string cs = lines[cy];
420 if (cs.length == 0 || cs[$-1] != ' ') lines[cy] ~= " ";
422 lines[cy] ~= s;
423 foreach (immutable c; cy+2..lines.length) lines[c-1] = lines[c];
424 lines.length -= 1;
425 lines.assumeSafeAppend;
428 void doEnter(bool forcequotes=true) () {
429 if (cy >= lines.length) {
430 lines ~= null;
431 cx = 0;
432 cy = cast(int)lines.length;
433 } else {
434 string s = lines[cy];
435 int ql = quoteLevel(cy);
436 int qlen = quoteLength(cy);
437 int ofs = lineofs(cx, cy);
438 lines.length += 1;
439 foreach_reverse (immutable c; cy+1..lines.length) lines[c] = lines[c-1];
440 lines[cy] = s[0..ofs];
441 if (qlen > 0 && (forcequotes || cx > 0)) {
442 lines[cy+1] = s[0..qlen]~s[ofs..$];
443 cx = qlen; // it is ok, quote chars are never multibyte
444 } else {
445 lines[cy+1] = s[ofs..$];
446 cx = 0;
448 ++cy;
452 void doBackspace () {
453 if (cy >= lines.length) {
454 if (cx > 0) --cx;
455 return;
456 } else if (cx > 0) {
457 string s = lines[cy];
458 int ofs1 = lineofs(cx, cy);
459 int ofs0 = lineofs(--cx, cy);
460 if (ofs1 > 0) {
461 lines[cy] = s[0..ofs0];
462 if (ofs1 < s.length) lines[cy] ~= s[ofs1..$];
463 return;
466 // join
467 if (cy > 0) {
468 --cy;
469 cx = linelen(cy);
470 doLineJoin();
474 void doDelete () {
475 void doJoin () {
476 if (cy >= lines.length || lines.length-cy < 2) return;
477 cx = linelen(cy);
478 doLineJoin();
481 if (cy < lines.length) {
482 string s = lines[cy];
483 int ofs0 = lineofs(cx, cy);
484 if (ofs0 >= s.length) { doJoin(); return; }
485 int ofs1 = lineofs(cx+1, cy);
486 if (ofs0 == ofs1) { doJoin(); return; }
487 lines[cy] = s[0..ofs0];
488 if (ofs1 < s.length) lines[cy] ~= s[ofs1..$];
492 void doLeft () {
493 int len = linelen(cy);
494 if (cx > len) cx = len;
495 if (cx == 0) {
496 if (cy > 0) {
497 --cy;
498 cx = linelen(cy);
500 } else {
501 --cx;
505 void doRight () {
506 auto len = linelen(cy);
507 if (cx > len) cx = len;
508 if (cx >= len) {
509 if (cy < lines.length) { ++cy; cx = 0; }
510 } else {
511 ++cx;
515 void doUp () {
516 if (cy > 0) --cy;
519 void doDown () {
520 if (cy < lines.length) ++cy;
523 void doHome () {
524 cx = 0;
527 void doEnd () {
528 cx = linelen(cy);
531 void gotoTop () {
532 cx = 0;
533 cy = 0;
536 void gotoBottom () {
537 cx = 0;
538 cy = cast(int)lines.length;
543 // ////////////////////////////////////////////////////////////////////////// //
544 struct QuoteInfo {
545 int level; // quote level
546 int length; // quote prefix length, in chars
549 QuoteInfo calcQuote(T:const(char)[]) (T s) {
550 static if (is(T == typeof(null))) {
551 return QuoteInfo();
552 } else {
553 QuoteInfo qi;
554 if (s.length > 0 && s[0] == '>') {
555 while (qi.length < s.length) {
556 if (s[qi.length] != ' ') {
557 if (s[qi.length] != '>') break;
558 ++qi.level;
560 ++qi.length;
562 if (s.length-qi.length > 1 && s[qi.length] == ' ') ++qi.length;
564 return qi;
569 // ////////////////////////////////////////////////////////////////////////// //
570 final class EditorWidget : Widget {
571 Editor editor;
572 int topline;
574 this (SubWindow aparent) {
575 super(aparent);
576 editor = new Editor();
579 private final void makeCurVisible () {
580 int lvis = wh/gxTextHeightUtf;
581 if (lvis < 1) lvis = 1; // just in case
582 int ltop = topline;
583 int lbot = topline+lvis-1;
584 int cy = editor.cury;
585 if (cy < ltop) {
586 topline = cy;
587 } else if (cy > lbot) {
588 topline = cy-lvis+1;
589 if (topline < 0) topline = 0;
593 private final void drawScrollBar () {
594 restoreClip(); // the easiest way again
595 int cx1 = x1+4;
596 int cy0 = y0;
597 int cy1 = y1;
598 // frame
599 gxVLine(cx1-2, cy0+1, wh-2, gxRGB!(220, 220, 220));
600 gxVLine(cx1-0, cy0+1, wh-2, gxRGB!(220, 220, 220));
601 gxHLine(cx1-1, cy0, 1, gxRGB!(220, 220, 220));
602 gxHLine(cx1-1, cy1, 1, gxRGB!(220, 220, 220));
603 int pix = (wh-2)*editor.cury/(editor.lineCount ? editor.lineCount : 1);
604 if (pix > wh-2) pix = wh-2;
605 gxVLine(cx1-1, cy0+1, pix, gxRGB!(160, 160, 160));
608 protected override void doPaint () {
609 makeCurVisible();
611 gxFillRect(x0, y0, ww, wh, gxRGB!(0, 0, 120));
613 int cx0 = x0;
614 int cx1 = x1;
615 int cy0 = y0;
616 int cy1 = y1;
618 int lidx = topline;
619 int y = cy0;
620 while (lidx < editor.lineCount && y <= cy1) {
621 string s = editor[lidx];
622 int qlevel = editor.quoteLevel(lidx);
623 uint clr = gxRGB!(220, 220, 0);
624 uint markFgClr = gxRGB!(255, 255, 255);
625 uint markBgClr = gxRGB!(0, 160, 160);
626 if (qlevel) {
627 final switch (qlevel%2) {
628 case 0: clr = gxRGB!(128, 128, 0); break;
629 case 1: clr = gxRGB!( 0, 128, 128); break;
632 string attname;
633 if (qlevel == 0) {
634 attname = editor.getAttachName(lidx);
635 if (attname.length) {
636 import std.file : exists, isFile;
637 clr = (attname.exists && attname.isFile ? gxRGB!(0x6e, 0x00, 0xff) : gxRGB!(0xff, 0x00, 0x00));
640 if (!editor.lineHasMark(lidx)) {
641 gxDrawTextUtf(cx0, y, s, clr);
642 } else {
643 int xx = cx0;
644 int cx = 0;
645 utfByLocal(editor[lidx], delegate (char ch) {
646 int w = gxCharWidthP(ch)+1;
647 if (editor.isMarked(cx, lidx)) {
648 gxFillRect(xx, y, w-(editor.isMarked(cx+1, lidx) ? 0 : 1), gxTextHeightUtf, markBgClr);
649 gxDrawCharP(xx, y, ch, markFgClr);
650 } else {
651 gxDrawCharP(xx, y, ch, clr);
653 xx += w;
654 ++cx;
657 if (active && lidx == editor.cury) {
658 int xpos = gxTextWidthUtf(s.utfleft(editor.curx));
659 drawTextCursor(parent.active, cx0+xpos, y);
661 ++lidx;
662 y += gxTextHeightUtf;
664 if (active && editor.cury >= editor.lineCount) {
665 drawTextCursor(parent.active, cx0, y);
667 drawScrollBar();
670 // return `true` if event was eaten
671 override bool onKey (KeyEvent event) {
672 if (!active) return false;
673 if (event.pressed) {
674 if (event == "Escape") { editor.putChar(editor.SpecCh.ResetMark); return true; }
675 if (event == "C-Space") { editor.putChar(editor.SpecCh.PutMark); return true; }
676 if (event == "Enter" || event == "PadEnter") { editor.putChar(editor.SpecCh.Enter); return true; }
677 if (event == "Left" || event == "Pad4") { editor.putChar(editor.SpecCh.Left); return true; }
678 if (event == "Right" || event == "Pad6") { editor.putChar(editor.SpecCh.Right); return true; }
679 if (event == "C-Left" || event == "C-Pad4") { editor.putChar(editor.SpecCh.WordLeft); return true; }
680 if (event == "C-Right" || event == "C-Pad6") { editor.putChar(editor.SpecCh.WordRight); return true; }
681 if (event == "Up" || event == "Pad8") { editor.putChar(editor.SpecCh.Up); return true; }
682 if (event == "Down" || event == "Pad2") { editor.putChar(editor.SpecCh.Down); return true; }
683 if (event == "Delete" || event == "PadDot") { editor.putChar(editor.SpecCh.Delete); return true; }
684 if (event == "Home" || event == "Pad7") { editor.putChar(editor.SpecCh.Home); return true; }
685 if (event == "End" || event == "Pad1") { editor.putChar(editor.SpecCh.End); return true; }
686 if (event == "C-Y") { editor.putChar(editor.SpecCh.KillLine); return true; }
687 if (event == "C-PageUp") { editor.gotoTop(); return true; }
688 if (event == "C-PageDown") { editor.gotoBottom(); return true; }
689 if (event == "PageUp") {
690 foreach (immutable _; 0..wh/gxTextHeightUtf) editor.putChar(editor.SpecCh.Up);
691 return true;
693 if (event == "PageDown") {
694 foreach (immutable _; 0..wh/gxTextHeightUtf) editor.putChar(editor.SpecCh.Down);
695 return true;
697 if (event == "S-Insert") {
698 getClipboardText(vbwin, delegate (in char[] text) { editor.putUtf(text[]); });
699 return true;
701 if (event == "C-Insert") {
702 string ct = editor.getSelectionText();
703 if (ct.length > 0) {
704 setClipboardText(vbwin, ct);
705 setPrimarySelection(vbwin, ct);
707 editor.putChar(editor.SpecCh.ResetMark);
708 return true;
710 if (event == "M-Tab") {
711 auto attname = editor.getAttachName(editor.cy);
712 if (attname.length && editor.cx >= editor.linelen(editor.cy)) {
713 auto cplist = buildAutoCompletion(attname);
714 auto adg = delegate (string s) {
715 //conwriteln("attname=[", attname, "]; s=[", s, "]");
716 if (s.length <= attname.length || s[0..attname.length] != attname) return;
717 foreach (char ch; s[attname.length..$]) editor.putChar(ch);
719 if (cplist.length == 1) {
720 adg(cplist[0]);
721 } else {
722 auto acw = new SelectCompletionWindow(attname, cplist, true);
723 acw.onSelected = adg;
726 return true;
729 return super.onKey(event);
732 override bool onMouse (MouseEvent event) {
733 if (!active) return false;
734 return super.onMouse(event);
737 override bool onChar (dchar ch) {
738 if (!active) return false;
739 if (ch == 8) { editor.putChar(ch); return true; }
740 if (ch >= ' ' && ch != 127) { editor.putChar(ch); return true; }
741 return super.onChar(ch);
745 public import mailedit;