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
;
32 // ////////////////////////////////////////////////////////////////////////// //
33 // very simple wrapping text editor, without much features
36 int linewrap
= 72; // "normal" line length
37 int maxlinelen
= 79; // maximum line length
42 int markx
, marky
= -1;
62 Delete
= cast(dchar)127,
66 @property bool hasMark () const pure nothrow @safe @nogc { return (marky
>= 0); }
68 int xy2pos (int x
, int y
) const pure nothrow @safe @nogc {
70 if (y
>= lines
.length
) y
= cast(int)lines
.length
;
72 foreach (immutable yy
; 0..y
-1) pos
+= linelen(yy
)+1; // 1 for virtual EOL
73 if (x
> 0 && y
< lines
.length
) {
77 if (dc
.decode(cast(ubyte)s
[0])) {
91 void addLine (const(char)[] s
) {
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;
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;
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;
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
);
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
);
133 if (y
!= marky
) return false;
134 if (cx
== markx
) return false;
135 return (cx
< markx ?
(x
>= cx
&& x
< markx
) : (x
>= markx
&& x
< cx
));
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;
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
; }
154 while (sx
!= ex || sy
!= ey
) {
155 dchar ch
= chatAt(sx
, sy
);
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;
174 while (pos
< s
.length
) {
175 if (s
[pos
] == ' ') { ++pos
; continue; }
176 if (s
[pos
] != '>') break;
179 if (pos
< s
.length
&& s
[pos
] == ' ') ++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;
189 if (s
[0] == ' ') { s
= s
[1..$]; continue; }
190 if (s
[0] != '>') break;
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;
206 if (dc
.decode(cast(ubyte)s
[0])) ++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
;
221 if (dc
.decode(cast(ubyte)s
[0])) {
229 dchar chatAt (int x
, int y
) const pure nothrow @safe @nogc {
230 if (x
< 0 || y
< 0 || y
>= lines
.length
) return 0;
232 if (s
.length
== 0 || x
>= s
.length
) return 0;
235 if (dc
.decode(cast(ubyte)s
[0])) {
236 if (x
-- == 0) return dc
.codepoint
;
243 // reformat whole text
248 while (cy
< lines
.length
) {
249 if (linelen(cy
) > maxlinelen
) {
250 int pos
= linewrap
-1;
251 while (pos
>= 0 && chatAt(pos
, cy
) > ' ') --pos
;
260 if (quoteLevel(cy
) != quoteLevel(cy
+1)) { ++cy
; continue; }
261 if (lines
[cy
].length
== 0 || lines
[cy
][$-1] > ' ') { ++cy
; continue; }
266 cy
= cast(int)lines
.length
;
269 void putUtf (const(char)[] s
) {
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
) {
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
:
288 if (cy
< lines
.length
) {
289 foreach (immutable c
; cy
+1..lines
.length
) lines
[c
-1] = lines
[c
];
291 lines
.assumeSafeAppend
;
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
:
303 bool inWord
= (chatAt(cx
, cy
) > ' ');
304 while (cx
!= 0 || cy
!= 0) {
306 if (chatAt(cx
, cy
) <= ' ') { doRight(); break; }
309 if (chatAt(cx
, cy
) <= ' ') doLeft(); else inWord
= true;
313 case SpecCh
.WordRight
:
315 bool inWord
= (chatAt(cx
, cy
) > ' ');
317 if (cy
>= lines
.length
) break;
318 if (cy
== lines
.length
-1 && cx
>= linelen(cy
)) break;
320 if (chatAt(cx
, cy
) <= ' ') break;
323 if (chatAt(cx
, cy
) <= ' ') doRight(); else inWord
= true;
329 if (ch
< ' ' || ch
== 127) return;
332 if (cy
>= lines
.length
) {
334 cy
= cast(int)lines
.length
;
338 int len
= linelen(cy
);
344 int ofs
= lineofs(cx
, cy
);
345 if (ofs
>= s
.length
) {
348 string t
= s
[0..ofs
];
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
;
368 if (lines
.length
-cy
< 2) break;
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;
376 // fix cursor coordinates
379 while (cy
< lines
.length
) {
381 if (cx
< linelen(cy
)) break;
383 if (cx
<= linelen(cy
)) break;
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;
397 while (pos
< s
.length
) {
398 if (s
[pos
] == ' ' || s
[pos
] == '>') {
405 if (pos
< s
.length
&& s
[pos
] == ' ') ++cx
;
410 if (cy
>= lines
.length || lines
.length
-cy
< 2) return;
412 string s
= lines
[cy
+1];
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
;
418 string cs
= lines
[cy
];
419 if (cs
.length
== 0 || cs
[$-1] != ' ') lines
[cy
] ~= " ";
422 foreach (immutable c
; cy
+2..lines
.length
) lines
[c
-1] = lines
[c
];
424 lines
.assumeSafeAppend
;
427 void doEnter(bool forcequotes
=true) () {
428 if (cy
>= lines
.length
) {
431 cy
= cast(int)lines
.length
;
433 string s
= lines
[cy
];
434 int ql
= quoteLevel(cy
);
435 int qlen
= quoteLength(cy
);
436 int ofs
= lineofs(cx
, cy
);
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
444 lines
[cy
+1] = s
[ofs
..$];
451 void doBackspace () {
452 if (cy
>= lines
.length
) {
456 string s
= lines
[cy
];
457 int ofs1
= lineofs(cx
, cy
);
458 int ofs0
= lineofs(--cx
, cy
);
460 lines
[cy
] = s
[0..ofs0
];
461 if (ofs1
< s
.length
) lines
[cy
] ~= s
[ofs1
..$];
475 if (cy
>= lines
.length || lines
.length
-cy
< 2) return;
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
..$];
492 int len
= linelen(cy
);
493 if (cx
> len
) cx
= len
;
505 auto len
= linelen(cy
);
506 if (cx
> len
) cx
= len
;
508 if (cy
< lines
.length
) { ++cy
; cx
= 0; }
519 if (cy
< lines
.length
) ++cy
;
537 cy
= cast(int)lines
.length
;
542 // ////////////////////////////////////////////////////////////////////////// //
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))) {
553 if (s
.length
> 0 && s
[0] == '>') {
554 while (qi
.length
< s
.length
) {
555 if (s
[qi
.length
] != ' ') {
556 if (s
[qi
.length
] != '>') break;
561 if (s
.length
-qi
.length
> 1 && s
[qi
.length
] == ' ') ++qi
.length
;
568 // ////////////////////////////////////////////////////////////////////////// //
569 final class EditorWidget
: Widget
{
573 this (SubWindow aparent
) {
575 editor
= new Editor();
578 private final void makeCurVisible () {
579 int lvis
= wh
/gxTextHeightUtf
;
580 if (lvis
< 1) lvis
= 1; // just in case
582 int lbot
= topline
+lvis
-1;
583 int cy
= editor
.cury
;
586 } else if (cy
> lbot
) {
588 if (topline
< 0) topline
= 0;
592 private final void drawScrollBar () {
593 restoreClip(); // the easiest way again
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 () {
610 gxFillRect(x0
, y0
, ww
, wh
, gxRGB
!(0, 0, 120));
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);
626 final switch (qlevel
%2) {
627 case 0: clr
= gxRGB
!(128, 128, 0); break;
628 case 1: clr
= gxRGB
!( 0, 128, 128); break;
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
);
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
);
650 gxDrawCharP(xx
, y
, ch
, clr
);
656 if (active
&& lidx
== editor
.cury
) {
657 int xpos
= gxTextWidthUtf(s
.utfleft(editor
.curx
));
658 drawTextCursor(cx0
+xpos
, y
);
662 y
+= gxTextHeightUtf
;
664 if (active
&& editor
.cury
>= editor
.lineCount
) {
665 drawTextCursor(cx0
, y
);
671 // return `true` if event was eaten
672 override bool onKey (KeyEvent event
) {
673 if (!active
) return false;
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
);
694 if (event
== "PageDown") {
695 foreach (immutable _
; 0..wh
/gxTextHeightUtf
) editor
.putChar(editor
.SpecCh
.Down
);
698 if (event
== "S-Insert") {
699 getClipboardText(vbwin
, delegate (in char[] text
) { editor
.putUtf(text
[]); });
702 if (event
== "C-Insert") {
703 string ct
= editor
.getSelectionText();
705 setClipboardText(vbwin
, ct
);
706 setPrimarySelection(vbwin
, ct
);
708 editor
.putChar(editor
.SpecCh
.ResetMark
);
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) {
723 auto acw
= new SelectCompletionWindow(attname
, cplist
, true);
724 acw
.onSelected
= adg
;
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
);