added update-in-progress notification window
[chiroptera.git] / editor.d
blobc5f901ea5030dfb1f58041257a9c037c81e77119
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;
20 import arsd.simpledisplay;
22 import iv.cmdcon;
23 import iv.strex;
24 import iv.utfutil;
25 import iv.vfs;
27 import egfx;
28 import egui;
29 import egui_dialogs;
32 // ////////////////////////////////////////////////////////////////////////// //
33 // very simple wrapping text editor, without much features
34 class Editor {
35 private:
36 int linewrap = 72; // "normal" line length
37 int maxlinelen = 79; // maximum line length
39 string[] lines;
41 int cx, cy;
42 int markx, marky = -1;
44 public:
45 enum SpecCh : dchar {
46 Left = '\x01',
47 Right = '\x02',
48 Up = '\x03',
49 Down = '\x04',
50 Home = '\x05',
51 End = '\x06',
52 PageUp = '\x07',
53 Backspace = '\x08',
54 Tab = '\x09',
55 Enter = '\x0a',
56 PageDown = '\x0b',
57 KillLine = '\x0c',
58 PutMark = '\x0e',
59 ResetMark = '\x0f',
60 WordLeft = '\x10',
61 WordRight = '\x11',
62 Delete = cast(dchar)127,
65 private:
66 @property bool hasMark () const pure nothrow @safe @nogc { return (marky >= 0); }
68 int xy2pos (int x, int y) const pure nothrow @safe @nogc {
69 if (y < 0) return 0;
70 if (y >= lines.length) y = cast(int)lines.length;
71 uint pos = 0;
72 foreach (immutable yy; 0..y-1) pos += linelen(yy)+1; // 1 for virtual EOL
73 if (x > 0 && y < lines.length) {
74 Utf8DecoderFast dc;
75 string s = lines[y];
76 while (s.length) {
77 if (dc.decode(cast(ubyte)s[0])) {
78 ++pos;
79 if (--x == 0) break;
81 s = s[1..$];
84 return pos;
87 public:
88 this () {
91 void addLine (const(char)[] s) {
92 lines ~= s.idup;
95 @property int lineCount () const pure nothrow @safe @nogc { return cast(int)lines.length; }
97 @property int curx () const pure nothrow @safe @nogc { return cx; }
98 @property int cury () const pure nothrow @safe @nogc { return cy; }
100 void normCX () pure nothrow @safe @nogc { if (cx > 0) { auto llen = linelen(cy); if (cx > llen) cx = llen; } }
102 // empty string: not an attach
103 string getAttachName (int ly) const pure nothrow @trusted @nogc {
104 if (ly < 0 || ly >= lines.length) return null;
105 string s = lines.ptr[ly];
106 if (s.length < 3 || s.ptr[0] != '@' || s.ptr[1] != '@') return null;
107 s = s[2..$].xstrip;
108 if (s.length == 0) return null;
109 // ok, it looks like attach, check next lines
110 foreach (string t; lines[ly..$]) {
111 if (t.length && (t.length < 2 || t.ptr[0] != '@' || t.ptr[0] != '@')) return null;
113 return s;
116 void resetMark () pure nothrow @safe @nogc { markx = 0; marky = -1; }
118 bool isMarked (int x, int y) const pure nothrow @safe @nogc {
119 if (!hasMark) return false;
120 if (cy < marky) {
121 if (y < cy || y > marky) return false;
122 if (y > cy && y < marky) return true;
123 if (y == cy) return (x >= cx);
124 if (y == marky) return (x < markx);
125 assert(0, "wtf?!");
126 } else if (cy > marky) {
127 if (y < marky || y > cy) return false;
128 if (y > marky && y < cy) return true;
129 if (y == marky) return (x >= markx);
130 if (y == cy) return (x < cx);
131 assert(0, "wtf?!");
132 } else {
133 if (y != marky) return false;
134 if (cx == markx) return false;
135 return (cx < markx ? (x >= cx && x < markx) : (x >= markx && x < cx));
137 assert(0, "wtf?!");
140 bool lineHasMark (int y) const pure nothrow @safe @nogc {
141 if (!hasMark) return false;
142 if (cy == marky) return (y == cy);
143 return (cy < marky ? (y >= cy && y <= marky) : (y >= marky && y <= cy));
146 string getSelectionText () const pure nothrow @safe {
147 if (!hasMark || (cx == markx && cy == marky)) return null;
148 int sx, sy, ex, ey;
149 if (cy < marky) { sx = cx; sy = cy; ex = markx; ey = marky; }
150 else if (cy > marky) { sx = markx; sy = marky; ex = cx; ey = cy; }
151 else if (cx < markx) { sx = cx; sy = cy; ex = markx; ey = marky; }
152 else { sx = markx; sy = marky; ex = cx; ey = cy; }
153 string res;
154 while (sx != ex || sy != ey) {
155 dchar ch = chatAt(sx, sy);
156 if (ch == 0) {
157 res ~= "\n";
158 sx = 0;
159 ++sy;
160 } else {
161 res ~= ch;
162 ++sx;
165 return res;
168 // return # of chars taken by quoting
169 int quoteLength (int lidx) const pure nothrow @safe @nogc {
170 if (lidx < 0 || lidx >= lines.length) return 0;
171 string s = lines[lidx];
172 if (s.length == 0 || s[0] != '>') return 0;
173 int pos = 0;
174 while (pos < s.length) {
175 if (s[pos] == ' ') { ++pos; continue; }
176 if (s[pos] != '>') break;
177 ++pos;
179 if (pos < s.length && s[pos] == ' ') ++pos;
180 return pos;
183 int quoteLevel (int lidx) const pure nothrow @safe @nogc {
184 if (lidx < 0 || lidx >= lines.length) return 0;
185 string s = lines[lidx];
186 if (s.length == 0 || s[0] != '>') return 0;
187 int res = 0;
188 while (s.length) {
189 if (s[0] == ' ') { s = s[1..$]; continue; }
190 if (s[0] != '>') break;
191 ++res;
192 s = s[1..$];
194 return res;
197 string opIndex (int idx) const pure nothrow @safe @nogc { return (idx >= 0 && idx < lines.length ? lines[idx] : null); }
199 int linelen (int idx) const pure nothrow @safe @nogc {
200 if (idx < 0 || idx >= lines.length) return 0;
201 string s = lines[idx];
202 if (s.length == 0) return 0;
203 Utf8DecoderFast dc;
204 int res = 0;
205 while (s.length) {
206 if (dc.decode(cast(ubyte)s[0])) ++res;
207 s = s[1..$];
209 return res;
212 // x position to line offset
213 int lineofs (int x, int idx) const pure nothrow @safe @nogc {
214 if (x <= 0 || idx < 0 || idx >= lines.length) return 0;
215 string s = lines[idx];
216 if (x >= s.length) return cast(int)s.length;
217 Utf8DecoderFast dc;
218 int res = 0;
219 while (s.length) {
220 ++res;
221 if (dc.decode(cast(ubyte)s[0])) {
222 if (--x == 0) break;
224 s = s[1..$];
226 return res;
229 dchar chatAt (int x, int y) const pure nothrow @safe @nogc {
230 if (x < 0 || y < 0 || y >= lines.length) return 0;
231 string s = lines[y];
232 if (s.length == 0 || x >= s.length) return 0;
233 Utf8DecoderFast dc;
234 while (s.length) {
235 if (dc.decode(cast(ubyte)s[0])) {
236 if (x-- == 0) return dc.codepoint;
238 s = s[1..$];
240 return 0;
243 // reformat whole text
244 void reformat () {
245 cx = 0;
246 cy = 0;
248 while (cy < lines.length) {
249 if (linelen(cy) > maxlinelen) {
250 int pos = linewrap-1;
251 while (pos >= 0 && chatAt(pos, cy) > ' ') --pos;
252 if (pos > 0) {
253 cx = ++pos;
254 doEnter();
255 } else {
256 ++cy;
259 // join and rewrap
260 if (quoteLevel(cy) != quoteLevel(cy+1)) { ++cy; continue; }
261 if (lines[cy].length == 0 || lines[cy][$-1] > ' ') { ++cy; continue; }
262 doLineJoin();
265 cx = 0;
266 cy = cast(int)lines.length;
269 void putUtf (const(char)[] s) {
270 Utf8DecoderFast dc;
271 foreach (char ch; s) {
272 if (dc.decode(cast(ubyte)ch)) {
273 if (dc.isValidDC(dc.codepoint)) putChar(dc.codepoint); else putChar('?');
278 void putChar (dchar ch) {
279 switch (ch) {
280 case SpecCh.Left: doLeft(); return;
281 case SpecCh.Right: doRight(); return;
282 case SpecCh.Up: doUp(); return;
283 case SpecCh.Down: doDown(); return;
284 case SpecCh.Home: doHome(); return;
285 case SpecCh.End: doEnd(); return;
286 case SpecCh.KillLine:
287 resetMark();
288 if (cy < lines.length) {
289 foreach (immutable c; cy+1..lines.length) lines[c-1] = lines[c];
290 lines.length -= 1;
291 lines.assumeSafeAppend;
292 cx = 0;
294 return;
295 case SpecCh.PutMark: markx = cx; marky = cy; return;
296 case SpecCh.ResetMark: resetMark(); return;
297 case 13: case 10: resetMark(); doEnter!false(); return;
298 case SpecCh.Backspace: resetMark(); doBackspace(); return;
299 case SpecCh.Delete: resetMark(); doDelete(); return;
300 case SpecCh.WordLeft:
301 normCX();
302 doLeft();
303 bool inWord = (chatAt(cx, cy) > ' ');
304 while (cx != 0 || cy != 0) {
305 if (inWord) {
306 if (chatAt(cx, cy) <= ' ') { doRight(); break; }
307 doLeft();
308 } else {
309 if (chatAt(cx, cy) <= ' ') doLeft(); else inWord = true;
312 break;
313 case SpecCh.WordRight:
314 normCX();
315 bool inWord = (chatAt(cx, cy) > ' ');
316 for (;;) {
317 if (cy >= lines.length) break;
318 if (cy == lines.length-1 && cx >= linelen(cy)) break;
319 if (inWord) {
320 if (chatAt(cx, cy) <= ' ') break;
321 doRight();
322 } else {
323 if (chatAt(cx, cy) <= ' ') doRight(); else inWord = true;
326 break;
327 default:
329 if (ch < ' ' || ch == 127) return;
330 resetMark();
331 string s;
332 if (cy >= lines.length) {
333 cx = 0;
334 cy = cast(int)lines.length;
335 s ~= ch;
336 lines ~= s;
337 } else {
338 int len = linelen(cy);
339 if (cx >= len) {
340 cx = len;
341 lines[cy] ~= ch;
342 } else {
343 s = lines[cy];
344 int ofs = lineofs(cx, cy);
345 if (ofs >= s.length) {
346 s ~= ch;
347 } else {
348 string t = s[0..ofs];
349 t ~= ch;
350 s = t~s[ofs..$];
351 lines[cy] = s;
355 doRight();
356 // wrapping (don't wrap attach lines)
357 if (linelen(cy) > linewrap && lines[cy][0..2] != "@@") {
358 int ocx = cx, ocy = cy;
359 while (cy < lines.length) {
360 if (linelen(cy) == 0) break; // stop right here, you criminal scum!
361 if (linelen(cy) > linewrap) {
362 int pos = linewrap-1;
363 while (pos >= 0 && chatAt(pos, cy) > ' ') --pos;
364 if (pos <= 0) break;
365 cx = ++pos;
366 doEnter();
367 } else {
368 if (lines.length-cy < 2) break;
369 // join and rewrap
370 if (quoteLevel(cy) != quoteLevel(cy+1)) break;
371 if (lines[cy].length == 0 || lines[cy][$-1] > ' ') break;
372 if (lines[cy+1].length == 0) break;
373 doLineJoin();
376 // fix cursor coordinates
377 cx = ocx;
378 cy = ocy;
379 while (cy < lines.length) {
380 if (cy == ocy) {
381 if (cx < linelen(cy)) break;
382 } else {
383 if (cx <= linelen(cy)) break;
385 cx -= linelen(cy);
386 ++cy;
387 skipQuotes();
392 private void skipQuotes () {
393 if (cy < 0 || cy >= lines.length) return;
394 string s = lines[cy];
395 if (s.length == 0 || s[0] != '>') return;
396 int pos = 0;
397 while (pos < s.length) {
398 if (s[pos] == ' ' || s[pos] == '>') {
399 ++cx;
400 ++pos;
401 } else {
402 break;
405 if (pos < s.length && s[pos] == ' ') ++cx;
408 void doLineJoin () {
409 if (cy < 0) cy = 0;
410 if (cy >= lines.length || lines.length-cy < 2) return;
411 cx = linelen(cy);
412 string s = lines[cy+1];
413 usize stpos = 0;
414 if (s.length > 0 && s[0] == '>') {
415 while (stpos < s.length) if (s[stpos] == ' ' || s[stpos] == '>') ++stpos; else break;
416 while (stpos < s.length && s[stpos] <= ' ') ++stpos;
417 s = s[stpos..$];
418 string cs = lines[cy];
419 if (cs.length == 0 || cs[$-1] != ' ') lines[cy] ~= " ";
421 lines[cy] ~= s;
422 foreach (immutable c; cy+2..lines.length) lines[c-1] = lines[c];
423 lines.length -= 1;
424 lines.assumeSafeAppend;
427 void doEnter(bool forcequotes=true) () {
428 if (cy >= lines.length) {
429 lines ~= null;
430 cx = 0;
431 cy = cast(int)lines.length;
432 } else {
433 string s = lines[cy];
434 int ql = quoteLevel(cy);
435 int qlen = quoteLength(cy);
436 int ofs = lineofs(cx, cy);
437 lines.length += 1;
438 foreach_reverse (immutable c; cy+1..lines.length) lines[c] = lines[c-1];
439 lines[cy] = s[0..ofs];
440 if (qlen > 0 && (forcequotes || cx > 0)) {
441 lines[cy+1] = s[0..qlen]~s[ofs..$];
442 cx = qlen; // it is ok, quote chars are never multibyte
443 } else {
444 lines[cy+1] = s[ofs..$];
445 cx = 0;
447 ++cy;
451 void doBackspace () {
452 if (cy >= lines.length) {
453 if (cx > 0) --cx;
454 return;
455 } else if (cx > 0) {
456 string s = lines[cy];
457 int ofs1 = lineofs(cx, cy);
458 int ofs0 = lineofs(--cx, cy);
459 if (ofs1 > 0) {
460 lines[cy] = s[0..ofs0];
461 if (ofs1 < s.length) lines[cy] ~= s[ofs1..$];
462 return;
465 // join
466 if (cy > 0) {
467 --cy;
468 cx = linelen(cy);
469 doLineJoin();
473 void doDelete () {
474 void doJoin () {
475 if (cy >= lines.length || lines.length-cy < 2) return;
476 cx = linelen(cy);
477 doLineJoin();
480 if (cy < lines.length) {
481 string s = lines[cy];
482 int ofs0 = lineofs(cx, cy);
483 if (ofs0 >= s.length) { doJoin(); return; }
484 int ofs1 = lineofs(cx+1, cy);
485 if (ofs0 == ofs1) { doJoin(); return; }
486 lines[cy] = s[0..ofs0];
487 if (ofs1 < s.length) lines[cy] ~= s[ofs1..$];
491 void doLeft () {
492 int len = linelen(cy);
493 if (cx > len) cx = len;
494 if (cx == 0) {
495 if (cy > 0) {
496 --cy;
497 cx = linelen(cy);
499 } else {
500 --cx;
504 void doRight () {
505 auto len = linelen(cy);
506 if (cx > len) cx = len;
507 if (cx >= len) {
508 if (cy < lines.length) { ++cy; cx = 0; }
509 } else {
510 ++cx;
514 void doUp () {
515 if (cy > 0) --cy;
518 void doDown () {
519 if (cy < lines.length) ++cy;
522 void doHome () {
523 cx = 0;
526 void doEnd () {
527 cx = linelen(cy);
530 void gotoTop () {
531 cx = 0;
532 cy = 0;
535 void gotoBottom () {
536 cx = 0;
537 cy = cast(int)lines.length;
542 // ////////////////////////////////////////////////////////////////////////// //
543 struct QuoteInfo {
544 int level; // quote level
545 int length; // quote prefix length, in chars
548 QuoteInfo calcQuote(T:const(char)[]) (T s) {
549 static if (is(T == typeof(null))) {
550 return QuoteInfo();
551 } else {
552 QuoteInfo qi;
553 if (s.length > 0 && s[0] == '>') {
554 while (qi.length < s.length) {
555 if (s[qi.length] != ' ') {
556 if (s[qi.length] != '>') break;
557 ++qi.level;
559 ++qi.length;
561 if (s.length-qi.length > 1 && s[qi.length] == ' ') ++qi.length;
563 return qi;
568 // ////////////////////////////////////////////////////////////////////////// //
569 final class EditorWidget : Widget {
570 Editor editor;
571 int topline;
573 this (SubWindow aparent) {
574 super(aparent);
575 editor = new Editor();
578 private final void makeCurVisible () {
579 int lvis = wh/gxTextHeightUtf;
580 if (lvis < 1) lvis = 1; // just in case
581 int ltop = topline;
582 int lbot = topline+lvis-1;
583 int cy = editor.cury;
584 if (cy < ltop) {
585 topline = cy;
586 } else if (cy > lbot) {
587 topline = cy-lvis+1;
588 if (topline < 0) topline = 0;
592 private final void drawScrollBar () {
593 restoreClip(); // the easiest way again
594 int cx1 = x1+4;
595 int cy0 = y0;
596 int cy1 = y1;
597 // frame
598 gxVLine(cx1-2, cy0+1, wh-2, gxRGB!(220, 220, 220));
599 gxVLine(cx1-0, cy0+1, wh-2, gxRGB!(220, 220, 220));
600 gxHLine(cx1-1, cy0, 1, gxRGB!(220, 220, 220));
601 gxHLine(cx1-1, cy1, 1, gxRGB!(220, 220, 220));
602 int pix = (wh-2)*editor.cury/(editor.lineCount ? editor.lineCount : 1);
603 if (pix > wh-2) pix = wh-2;
604 gxVLine(cx1-1, cy0+1, pix, gxRGB!(160, 160, 160));
607 protected override void doPaint () {
608 makeCurVisible();
610 gxFillRect(x0, y0, ww, wh, gxRGB!(0, 0, 120));
612 int cx0 = x0;
613 int cx1 = x1;
614 int cy0 = y0;
615 int cy1 = y1;
617 int lidx = topline;
618 int y = cy0;
619 while (lidx < editor.lineCount && y <= cy1) {
620 string s = editor[lidx];
621 int qlevel = editor.quoteLevel(lidx);
622 uint clr = gxRGB!(220, 220, 0);
623 uint markFgClr = gxRGB!(255, 255, 255);
624 uint markBgClr = gxRGB!(0, 160, 160);
625 if (qlevel) {
626 final switch (qlevel%2) {
627 case 0: clr = gxRGB!(128, 128, 0); break;
628 case 1: clr = gxRGB!( 0, 128, 128); break;
631 string attname;
632 if (qlevel == 0) {
633 attname = editor.getAttachName(lidx);
634 if (attname.length) {
635 import std.file : exists, isFile;
636 clr = (attname.exists && attname.isFile ? gxRGB!(0x6e, 0x00, 0xff) : gxRGB!(0xff, 0x00, 0x00));
639 if (!editor.lineHasMark(lidx)) {
640 gxDrawTextUtf(cx0, y, s, clr);
641 } else {
642 int xx = cx0;
643 int cx = 0;
644 utfByLocal(editor[lidx], delegate (char ch) {
645 int w = gxCharWidthP(ch)+1;
646 if (editor.isMarked(cx, lidx)) {
647 gxFillRect(xx, y, w-(editor.isMarked(cx+1, lidx) ? 0 : 1), gxTextHeightUtf, markBgClr);
648 gxDrawCharP(xx, y, ch, markFgClr);
649 } else {
650 gxDrawCharP(xx, y, ch, clr);
652 xx += w;
653 ++cx;
656 if (active && lidx == editor.cury) {
657 int xpos = gxTextWidthUtf(s.utfleft(editor.curx));
658 drawTextCursor(cx0+xpos, y);
659 postCurBlink();
661 ++lidx;
662 y += gxTextHeightUtf;
664 if (active && editor.cury >= editor.lineCount) {
665 drawTextCursor(cx0, y);
666 postCurBlink();
668 drawScrollBar();
671 // return `true` if event was eaten
672 override bool onKey (KeyEvent event) {
673 if (!active) return false;
674 if (event.pressed) {
675 if (event == "Escape") { editor.putChar(editor.SpecCh.ResetMark); return true; }
676 if (event == "C-Space") { editor.putChar(editor.SpecCh.PutMark); return true; }
677 if (event == "Enter" || event == "PadEnter") { editor.putChar(editor.SpecCh.Enter); return true; }
678 if (event == "Left" || event == "Pad4") { editor.putChar(editor.SpecCh.Left); return true; }
679 if (event == "Right" || event == "Pad6") { editor.putChar(editor.SpecCh.Right); return true; }
680 if (event == "C-Left" || event == "C-Pad4") { editor.putChar(editor.SpecCh.WordLeft); return true; }
681 if (event == "C-Right" || event == "C-Pad6") { editor.putChar(editor.SpecCh.WordRight); return true; }
682 if (event == "Up" || event == "Pad8") { editor.putChar(editor.SpecCh.Up); return true; }
683 if (event == "Down" || event == "Pad2") { editor.putChar(editor.SpecCh.Down); return true; }
684 if (event == "Delete" || event == "PadDot") { editor.putChar(editor.SpecCh.Delete); return true; }
685 if (event == "Home" || event == "Pad7") { editor.putChar(editor.SpecCh.Home); return true; }
686 if (event == "End" || event == "Pad1") { editor.putChar(editor.SpecCh.End); return true; }
687 if (event == "C-Y") { editor.putChar(editor.SpecCh.KillLine); return true; }
688 if (event == "C-PageUp") { editor.gotoTop(); return true; }
689 if (event == "C-PageDown") { editor.gotoBottom(); return true; }
690 if (event == "PageUp") {
691 foreach (immutable _; 0..wh/gxTextHeightUtf) editor.putChar(editor.SpecCh.Up);
692 return true;
694 if (event == "PageDown") {
695 foreach (immutable _; 0..wh/gxTextHeightUtf) editor.putChar(editor.SpecCh.Down);
696 return true;
698 if (event == "S-Insert") {
699 getClipboardText(vbwin, delegate (in char[] text) { editor.putUtf(text[]); });
700 return true;
702 if (event == "C-Insert") {
703 string ct = editor.getSelectionText();
704 if (ct.length > 0) {
705 setClipboardText(vbwin, ct);
706 setPrimarySelection(vbwin, ct);
708 editor.putChar(editor.SpecCh.ResetMark);
709 return true;
711 if (event == "M-Tab") {
712 auto attname = editor.getAttachName(editor.cy);
713 if (attname.length && editor.cx >= editor.linelen(editor.cy)) {
714 auto cplist = buildAutoCompletion(attname);
715 auto adg = delegate (string s) {
716 //conwriteln("attname=[", attname, "]; s=[", s, "]");
717 if (s.length <= attname.length || s[0..attname.length] != attname) return;
718 foreach (char ch; s[attname.length..$]) editor.putChar(ch);
720 if (cplist.length == 1) {
721 adg(cplist[0]);
722 } else {
723 auto acw = new SelectCompletionWindow(attname, cplist, true);
724 acw.onSelected = adg;
727 return true;
730 return super.onKey(event);
733 override bool onMouse (MouseEvent event) {
734 if (!active) return false;
735 return super.onMouse(event);
738 override bool onChar (dchar ch) {
739 if (!active) return false;
740 if (ch == 8) { editor.putChar(ch); return true; }
741 if (ch >= ' ' && ch != 127) { editor.putChar(ch); return true; }
742 return super.onChar(ch);