1 /* Invisible Vector Library
2 * simple FlexBox-based TUI engine
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, version 3 of the License ONLY.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 module egui
.editor
/*is aliced*/;
18 import arsd
.simpledisplay
;
26 import iv
.egeditor
.editor
;
27 //import iv.egeditor.highlighters;
31 import egui
.subwindows
;
36 // ////////////////////////////////////////////////////////////////////////// //
37 final class ChiTextMeter
: EgTextMeter
{
40 //int currofs; /// x offset for current char (i.e. the last char that was passed to `advance()` should be drawn with this offset)
41 //int currwdt; /// current line width (including, the last char that was passed to `advance()`), preferably without trailing empty space between chars
42 //int currheight; /// current text height; keep this in sync with the current state; `reset` should set it to "default text height"
44 /// this should reset text width iterator (and curr* fields); tabsize > 0: process tabs as... well... tabs ;-)
45 override void reset (int tabsize
) nothrow {
46 twkern
.reset(tabsize
);
47 currheight
= gxTextHeightUtf
;
50 /// advance text width iterator, return x position for drawing next char
51 override void advance (dchar ch
, in ref GapBuffer
.HighState hs
) nothrow {
52 twkern
.fixWidthPre(ch
);
53 currofs
= twkern
.currOfs
;
54 currwdt
= twkern
.nextOfsNoSpacing
;
57 /// finish text iterator; it should NOT reset curr* fields!
58 /// WARNING: EditorEngine tries to call this after each `reset()`, but user code may not
59 override void finish () nothrow {
60 //return twkern.finalWidth;
65 // ////////////////////////////////////////////////////////////////////////// //
66 final class TextEditor
: EditorEngine
{
67 enum TEDSingleOnly
; // only for single-line mode
68 enum TEDMultiOnly
; // only for multiline mode
69 enum TEDEditOnly
; // only for non-readonly mode
70 enum TEDROOnly
; // only for readonly mode
72 static struct TEDKey
{ string key
; string help
; bool hidden
; } // UDA
74 static string
TEDImplX(string key
, string help
, string code
, size_t ln
) () {
75 static assert(key
.length
> 0, "wtf?!");
76 static assert(code
.length
> 0, "wtf?!");
77 string res
= "@TEDKey("~key
.stringof
~", "~help
.stringof
~") void _ted_";
79 while (pos
< key
.length
) {
81 if (key
.length
-pos
> 0 && key
[pos
] == '-') {
82 if (ch
== 'C' || ch
== 'c') { ++pos
; res
~= "Ctrl"; continue; }
83 if (ch
== 'M' || ch
== 'm') { ++pos
; res
~= "Alt"; continue; }
84 if (ch
== 'S' || ch
== 's') { ++pos
; res
~= "Shift"; continue; }
86 if (ch
== '^') { res
~= "Ctrl"; continue; }
87 if (ch
>= 'a' && ch
<= 'z') ch
-= 32;
88 if ((ch
>= '0' && ch
<= '9') ||
(ch
>= 'A' && ch
<= 'Z') || ch
== '_') res
~= ch
; else res
~= '_';
91 res
~= " () {"~code
~"}";
95 mixin template TEDImpl(string key
, string help
, string code
, size_t ln
=__LINE__
) {
96 mixin(TEDImplX
!(key
, help
, code
, ln
));
99 mixin template TEDImpl(string key
, string code
, size_t ln
=__LINE__
) {
100 mixin(TEDImplX
!(key
, "", code
, ln
));
104 KeyEvent
[32] comboBuf
;
105 int comboCount
; // number of items in `comboBuf`
108 ChiTextMeter chiTextMeter
;
111 this (Widget apw
, int x0
, int y0
, int w
, int h
, bool asinglesine
=false) {
113 //coordsInPixels = true;
114 lineHeightPixels
= gxTextHeightUtf
;
115 chiTextMeter
= new ChiTextMeter();
116 super(x0
, y0
, w
, h
, null, asinglesine
);
117 textMeter
= chiTextMeter
;
120 tabsize
= 4; // spaces
123 final int lineQuoteLevel (int lidx
) {
124 if (lidx
< 0 || lidx
>= lc
.linecount
) return 0;
125 int pos
= lc
.line2pos(lidx
);
126 auto ts
= gb
.textsize
;
127 if (pos
>= ts
) return 0;
128 if (gb
[pos
] != '>') return 0;
133 if (ch
== '>') ++count
;
134 else if (ch
== '\n') break;
135 else if (ch
!= ' ') break;
140 public override void drawCursor () {
143 localCursorXY(&lcx
, &lcy
);
144 drawTextCursor(pw
.parent
.active
, x0
+lcx
, y0
+lcy
);
148 // not here, 'cause this is done before text
149 public override void drawStatus () {}
151 public final paintStatusLine () {
152 import core
.stdc
.stdio
: snprintf
;
153 int sx
= x0
, sy
= y0
+height
;
154 gxFillRect(sx
, sy
, width
, gxTextHeightUtf
, gxRGB
!(255, 255, 255));
155 char[128] buf
= void;
156 auto len
= snprintf(buf
.ptr
, buf
.length
, "%04d:%04d %d", curx
, cury
, linecount
);
157 gxDrawTextUtf(sx
+2, sy
, buf
[0..len
], gxRGB
!(0, 0, 0));
160 public override void drawPage () {
161 fullDirty(); // HACK!
163 gxClipRect
.intersect(GxRect(x0
, y0
, width
, height
));
166 if (!singleline
) paintStatusLine();
170 public override void drawLine (int lidx
, int yofs
, int xskip
) {
173 auto pos
= lc
.line2pos(lidx
);
174 auto lea = lc
.line2pos(lidx
+1);
175 auto ts
= gb
.textsize
;
176 bool utfucked
= utfuck
;
177 immutable int bs
= bstart
, be
= bend
;
180 uint clr
= gxRGB
!(220, 220, 0);
181 enum markFgClr
= gxRGB
!(255, 255, 255);
182 enum markBgClr
= gxRGB
!(0, 160, 160);
185 int qlevel
= lineQuoteLevel(lidx
);
187 final switch (qlevel
%2) {
188 case 0: clr
= gxRGB
!(128, 128, 0); break;
189 case 1: clr
= gxRGB
!( 0, 128, 128); break;
192 char[1024] abuf
= void;
193 auto aname
= getAttachName(abuf
[], lidx
);
196 import std
.file
: exists
, isFile
;
197 clr
= (aname
.exists
&& aname
.isFile ? gxRGB
!(0x6e, 0x00, 0xff) : gxRGB
!(0xff, 0x00, 0x00));
198 } catch (Exception e
) {}
203 bool checkMarking
= false;
204 if (hasMarkedBlock
) {
205 //conwriteln("bs=", bs, "; be=", be, "; pos=", pos, "; lea=", lea);
206 if (pos
>= bs
&& lea <= be
) {
208 gxFillRect(x0
, y
, winw
, lineHeightPixels
, markBgClr
);
210 } else if (pos
< be
&& lea > bs
) {
211 // draw block background
213 int bx0
= x
, bx1
= x
;
214 chiTextMeter
.twkern
.reset(visualtabs ? tabsize
: 0);
216 immutable dchar dch
= dcharAtAdvance(tpos
);
217 if (!singleline
&& dch
== '\n') break;
218 chiTextMeter
.twkern
.fixWidthPre(dch
);
219 if (tpos
> bs
) break;
221 bx0
= bx1
= x
+chiTextMeter
.twkern
.currOfs
;
224 if (tpos
>= be
) break;
225 immutable dchar dch
= dcharAtAdvance(tpos
);
226 if (!singleline
&& dch
== '\n') { eolhit
= (tpos
< be
); break; }
227 chiTextMeter
.twkern
.fixWidthPre(dch
);
228 if (tpos
> be
) break;
230 bx1
= (eolhit ? x0
+width
: x
+chiTextMeter
.twkern
.finalWidth
);
231 gxFillRect(bx0
, y
, bx1
-bx0
+1, lineHeightPixels
, markBgClr
);
235 if (singleline
&& hasMarkedBlock
) checkMarking
= true; // let it be
237 //twkern.reset(visualtabs ? tabsize : 0);
244 while (epos
< ts
) if (gb
[epos
++] == '\n') { --epos
; break; }
248 int wdt
= gxDrawTextUtf(GxDrawTextOptions
.Tab(tabsize
), x
, y
, this[pos
..epos
], delegate (in ref state
) nothrow @trusted {
249 return (checkMarking ?
(pos
+state
.spos
>= bs
&& pos
+state
.epos
<= be ? markFgClr
: clr
) : clr
);
252 if (epos
> pos
&& gb
[epos
-1] == ' ') gxDrawChar(x
+wdt
, y
, '\u2248', gxRGB
!(0, 90, 220));
257 if (checkMarking) cc = (pos >= bs && pos < be ? markFgClr : clr);
258 immutable dchar dch = dcharAtAdvance(pos);
259 if (!singleline && dch == '\n') {
260 // draw "can wrap" mark
261 if (pos-2 >= ls && gb[pos-2] == ' ') {
262 int xx = x+twkern.finalWidth+1;
263 gxDrawChar(xx, y, '\n', gxRGB!(0, 0, 220));
267 if (dch == '\t' && visualtabs) {
268 int xx = x+twkern.fixWidthPre('\t');
269 gxHLine(xx, y+lineHeightPixels/2, twkern.tablength, gxRGB!(200, 0, 0));
271 gxDrawChar(x+twkern.fixWidthPre(dch), y, dch, cc);
278 // use `winXXX` vars to know window dimensions
279 public override void drawEmptyLine (int yofs
) {
280 // nothing to do here
283 protected enum Ecc
{ None
, Eaten
, Combo
}
287 // Combo: combo start
288 // comboBuf should contain comboCount keys!
289 protected final Ecc
checkKeys (const(char)[] keys
) {
290 foreach (immutable cidx
; 0..comboCount
+1) {
292 if (keys
.length
== 0) return Ecc
.Combo
;
294 while (kepos
< keys
.length
&& keys
.ptr
[kepos
] > ' ') ++kepos
;
295 if (comboBuf
[cidx
] != keys
[0..kepos
]) return Ecc
.None
;
296 keys
= keys
[kepos
..$];
298 return (keys
.xstrip
.length ? Ecc
.Combo
: Ecc
.Eaten
);
301 // fuck! `(this ME)` trick doesn't work here
302 protected final Ecc
doEditorCommandByUDA(ME
=typeof(this)) (KeyEvent key
) {
304 bool possibleCombo
= false;
305 // temporarily add current key to combo
306 comboBuf
[comboCount
] = key
;
307 // check all known combos
308 foreach (string memn
; __traits(allMembers
, ME
)) {
309 static if (is(typeof(&__traits(getMember
, ME
, memn
)))) {
310 import std
.meta
: AliasSeq
;
311 alias mx
= AliasSeq
!(__traits(getMember
, ME
, memn
))[0];
312 static if (isCallable
!mx
&& hasUDA
!(mx
, TEDKey
)) {
314 bool goodMode
= true;
315 static if (hasUDA
!(mx
, TEDSingleOnly
)) { if (!singleline
) goodMode
= false; }
316 static if (hasUDA
!(mx
, TEDMultiOnly
)) { if (singleline
) goodMode
= false; }
317 static if (hasUDA
!(mx
, TEDEditOnly
)) { if (readonly
) goodMode
= false; }
318 static if (hasUDA
!(mx
, TEDROOnly
)) { if (!readonly
) goodMode
= false; }
320 foreach (const TEDKey attr
; getUDAs
!(mx
, TEDKey
)) {
321 auto cc
= checkKeys(attr
.key
);
322 if (cc
== Ecc
.Eaten
) {
324 static if (is(ReturnType
!mx
== void)) {
325 comboCount
= 0; // reset combo
330 comboCount
= 0; // reset combo
334 } else if (cc
== Ecc
.Combo
) {
335 possibleCombo
= true;
342 // check if we can start/continue combo
344 if (++comboCount
< comboBuf
.length
-1) return Ecc
.Combo
;
346 // if we have combo prefix, eat key unconditionally
347 if (comboCount
> 0) {
348 comboCount
= 0; // reset combo, too long, or invalid, or none
354 bool processKey (KeyEvent key
) {
355 final switch (doEditorCommandByUDA(key
)) {
356 case Ecc
.None
: break;
365 bool processChar (dchar ch
) {
366 if (ch
< ' ' || ch
== 127) return false;
368 // check if we should reformat
369 auto llen
= linelen(cury
);
371 if (curx
== 0 || gb
[curpos
-1] != ' ') doPutChar(' ');
376 int rpos
= lc
.linestart(ocy
);
378 if (gb
[rpos
] == '>') {
379 while (gb
[rpos
] == '>') { ++qcnt
; ++rpos
; }
380 if (gb
[rpos
] == ' ') { ++qcnt
; ++rpos
; }
382 //conwriteln("origcx=", ocx, "; ncx=", ocx-qcnt, "; qcnt=", qcnt);
383 if (ocx
< qcnt
) ocx
= qcnt
; else ocx
-= qcnt
;
385 reformatFromLine
!true(cury
);
387 while (ocy
< lc
.linecount
) {
388 int rpos
= lc
.linestart(ocy
);
390 //conwriteln(" 00: ocy=", ocy, "; llen=", llen, "; ocx=", ocx);
392 if (gb
[rpos
] == '>') {
393 while (gb
[rpos
] == '>') { --llen
; ++qcnt
; ++rpos
; }
394 if (gb
[rpos
] == ' ') { --llen
; ++qcnt
; ++rpos
; }
396 //conwriteln(" 01: ocy=", ocy, "; llen=", llen, "; ocx=", ocx, "; qcnt=", qcnt);
397 if (ocx
<= llen
) { ocx
+= qcnt
; break; }
409 bool processClick (int x
, int y
, MouseEvent event
) {
410 if (x
< 0 || y
< 0 || x
>= winw || y
>= winh
) return false;
411 if (event
.type
== MouseEventType
.buttonPressed
&& event
.button
== MouseButton
.left
) {
413 widget2text(x
, y
, tx
, ty
);
421 void processWordWith (scope char delegate (char ch
) dg
) {
422 if (dg
is null) return;
423 bool undoAdded
= false;
424 scope(exit
) if (undoAdded
) undoGroupEnd();
426 if (!isWordChar(gb
[pos
])) return;
428 while (pos
> 0 && isWordChar(gb
[pos
-1])) --pos
;
429 while (pos
< gb
.textsize
) {
431 if (!isWordChar(gb
[pos
])) break;
434 if (!undoAdded
) { undoAdded
= true; undoGroupStart(); }
435 replaceText
!"none"(pos
, 1, (&nc
)[0..1]);
443 @TEDMultiOnly mixin TEDImpl
!("Up", q
{ doUp(); });
444 @TEDMultiOnly mixin TEDImpl
!("S-Up", q
{ doUp(true); });
445 @TEDMultiOnly mixin TEDImpl
!("C-Up", q
{ doScrollUp(); });
446 @TEDMultiOnly mixin TEDImpl
!("S-C-Up", q
{ doScrollUp(true); });
448 @TEDMultiOnly mixin TEDImpl
!("Down", q
{ doDown(); });
449 @TEDMultiOnly mixin TEDImpl
!("S-Down", q
{ doDown(true); });
450 @TEDMultiOnly mixin TEDImpl
!("C-Down", q
{ doScrollDown(); });
451 @TEDMultiOnly mixin TEDImpl
!("S-C-Down", q
{ doScrollDown(true); });
453 mixin TEDImpl
!("Left", q
{ doLeft(); });
454 mixin TEDImpl
!("S-Left", q
{ doLeft(true); });
455 mixin TEDImpl
!("C-Left", q
{ doWordLeft(); });
456 mixin TEDImpl
!("S-C-Left", q
{ doWordLeft(true); });
458 mixin TEDImpl
!("Right", q
{ doRight(); });
459 mixin TEDImpl
!("S-Right", q
{ doRight(true); });
460 mixin TEDImpl
!("C-Right", q
{ doWordRight(); });
461 mixin TEDImpl
!("S-C-Right", q
{ doWordRight(true); });
463 @TEDMultiOnly mixin TEDImpl
!("PageUp", q
{ doPageUp(); });
464 @TEDMultiOnly mixin TEDImpl
!("S-PageUp", q
{ doPageUp(true); });
465 @TEDMultiOnly mixin TEDImpl
!("C-PageUp", q
{ doTextTop(); });
466 @TEDMultiOnly mixin TEDImpl
!("S-C-PageUp", q
{ doTextTop(true); });
468 @TEDMultiOnly mixin TEDImpl
!("PageDown", q
{ doPageDown(); });
469 @TEDMultiOnly mixin TEDImpl
!("S-PageDown", q
{ doPageDown(true); });
470 @TEDMultiOnly mixin TEDImpl
!("C-PageDown", q
{ doTextBottom(); });
471 @TEDMultiOnly mixin TEDImpl
!("S-C-PageDown", q
{ doTextBottom(true); });
473 mixin TEDImpl
!("Home", q
{ doHome(); });
474 mixin TEDImpl
!("S-Home", q
{ doHome(true, true); });
475 @TEDMultiOnly mixin TEDImpl
!("C-Home", q
{ doPageTop(); });
476 @TEDMultiOnly mixin TEDImpl
!("S-C-Home", q
{ doPageTop(true); });
478 mixin TEDImpl
!("End", q
{ doEnd(); });
479 mixin TEDImpl
!("S-End", q
{ doEnd(true); });
480 @TEDMultiOnly mixin TEDImpl
!("C-End", q
{ doPageBottom(); });
481 @TEDMultiOnly mixin TEDImpl
!("S-C-End", q
{ doPageBottom(true); });
483 @TEDEditOnly mixin TEDImpl
!("Backspace", q
{ doBackspace(); });
484 @TEDSingleOnly @TEDEditOnly mixin TEDImpl
!("M-Backspace", "delete previous word", q
{ doDeleteWord(); });
485 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("M-Backspace", "delete previous word or unindent", q
{ doBackByIndent(); });
487 mixin TEDImpl
!("Delete", q
{
491 //mixin TEDImpl!("^Insert", "copy block to clipboard file, reset block mark", q{ if (tempBlockFileName.length == 0) return; doBlockWrite(tempBlockFileName); doBlockResetMark(); });
493 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("Enter", q
{
495 if (lineQuoteLevel(ly
)) {
496 doLineSplit(false); // no autoindent
497 auto ls
= lc
.linestart(ly
);
499 scope(exit
) undoGroupEnd();
500 while (gb
[ls
] == '>') {
502 insertText
!"end"(curpos
, ">");
504 if (gb
[curpos
] > ' ') insertText
!"end"(curpos
, " ");
509 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("M-Enter", "split line without autoindenting", q
{ doLineSplit(false); });
512 mixin TEDImpl
!("F3", "start/stop/reset block marking", q
{ doToggleBlockMarkMode(); });
513 mixin TEDImpl
!("C-F3", "reset block mark", q
{ doBlockResetMark(); });
514 @TEDEditOnly mixin TEDImpl
!("F5", "copy block", q
{ doBlockCopy(); });
515 // mixin TEDImpl!("^F5", "copy block to clipboard file", q{ if (tempBlockFileName.length == 0) return; doBlockWrite(tempBlockFileName); });
516 //@TEDEditOnly mixin TEDImpl!("S-F5", "insert block from clipboard file", q{ if (tempBlockFileName.length == 0) return; waitingInF5 = true; });
517 @TEDEditOnly mixin TEDImpl
!("F6", "move block", q
{ doBlockMove(); });
518 @TEDEditOnly mixin TEDImpl
!("F8", "delete block", q
{ doBlockDelete(); });
520 mixin TEDImpl
!("C-A", "move to line start", q
{ doHome(); });
521 mixin TEDImpl
!("C-E", "move to line end", q
{ doEnd(); });
523 @TEDMultiOnly mixin TEDImpl
!("M-I", "jump to previous bookmark", q
{ doBookmarkJumpUp(); });
524 @TEDMultiOnly mixin TEDImpl
!("M-J", "jump to next bookmark", q
{ doBookmarkJumpDown(); });
525 @TEDMultiOnly mixin TEDImpl
!("M-K", "toggle bookmark", q
{ doBookmarkToggle(); });
527 @TEDEditOnly mixin TEDImpl
!("M-C", "capitalize word", q
{
529 processWordWith((char ch
) {
530 if (first
) { first
= false; ch
= ch
.toupper
; }
534 @TEDEditOnly mixin TEDImpl
!("M-Q", "lowercase word", q
{ processWordWith((char ch
) => ch
.tolower
); });
535 @TEDEditOnly mixin TEDImpl
!("M-U", "uppercase word", q
{ processWordWith((char ch
) => ch
.toupper
); });
537 @TEDMultiOnly mixin TEDImpl
!("M-S-L", "force center current line", q
{ makeCurLineVisibleCentered(true); });
538 @TEDEditOnly mixin TEDImpl
!("C-U", "undo", q
{ doUndo(); });
539 @TEDEditOnly mixin TEDImpl
!("M-S-U", "redo", q
{ doRedo(); });
540 @TEDEditOnly mixin TEDImpl
!("C-W", "remove previous word", q
{ doDeleteWord(); });
541 @TEDEditOnly mixin TEDImpl
!("C-Y", "remove current line", q
{ doKillLine(); });
543 //@TEDMultiOnly @TEDEditOnly mixin TEDImpl!("Tab", q{ doPutText(" "); });
544 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-Tab", "indent block", q
{ doIndentBlock(); });
545 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-S-Tab", "unindent block", q
{ doUnindentBlock(); });
547 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K C-I", "indent block", q
{ doIndentBlock(); });
548 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K C-U", "unindent block", q
{ doUnindentBlock(); });
549 @TEDEditOnly mixin TEDImpl
!("C-K C-E", "clear from cursor to EOL", q
{ doKillToEOL(); });
550 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K Tab", "indent block", q
{ doIndentBlock(); });
551 // @TEDEditOnly mixin TEDImpl!("^K M-Tab", "untabify", q{ doUntabify(gb.tabsize ? gb.tabsize : 2); }); // alt+tab: untabify
552 // @TEDEditOnly mixin TEDImpl!("^K C-space", "remove trailing spaces", q{ doRemoveTailingSpaces(); });
553 // mixin TEDImpl!("C-K C-T", /*"toggle \"visual tabs\" mode",*/ q{ visualtabs = !visualtabs; });
555 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K C-B", q
{ doSetBlockStart(); });
556 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K C-K", q
{ doSetBlockEnd(); });
558 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K C-C", q
{ doBlockCopy(); });
559 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K C-M", q
{ doBlockMove(); });
560 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K C-Y", q
{ doBlockDelete(); });
561 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K C-H", q
{ doBlockResetMark(); });
563 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K Backspace", q
{ doBlockResetMark(); });
565 @TEDEditOnly mixin TEDImpl
!("C-Q Tab", q
{ doPutChar('\t'); });
566 //mixin TEDImpl!("C-Q C-U", "toggle utfuck mode", q{ utfuck = !utfuck; }); // ^Q^U: switch utfuck mode
567 //mixin TEDImpl!("C-Q 1", "switch to koi8", q{ utfuck = false; codepage = CodePage.koi8u; fullDirty(); });
568 //mixin TEDImpl!("C-Q 2", "switch to cp1251", q{ utfuck = false; codepage = CodePage.cp1251; fullDirty(); });
569 //mixin TEDImpl!("C-Q 3", "switch to cp866", q{ utfuck = false; codepage = CodePage.cp866; fullDirty(); });
570 mixin TEDImpl
!("C-Q C-B", "go to block start", q
{ if (hasMarkedBlock
) gotoPos
!true(bstart
); lastBGEnd
= false; });
572 @TEDSingleOnly @TEDEditOnly mixin TEDImpl
!("C-Q Enter", "intert LF", q
{ doPutChar('\n'); });
573 @TEDSingleOnly @TEDEditOnly mixin TEDImpl
!("C-Q M-Enter", "intert CR", q
{ doPutChar('\r'); });
575 mixin TEDImpl
!("C-Q C-K", "go to block end", q
{ if (hasMarkedBlock
) gotoPos
!true(bend
); lastBGEnd
= true; });
577 @TEDMultiOnly @TEDROOnly mixin TEDImpl
!("Space", q
{ doPageDown(); });
578 @TEDMultiOnly @TEDROOnly mixin TEDImpl
!("S-Space", q
{ doPageUp(); });
581 string
[] extractAttaches () {
584 lineloop
: while (lidx
< lc
.linecount
) {
586 auto pos
= lc
.linestart(lidx
);
587 //conwriteln("checking line ", lidx, ": '", gb[pos], "'");
588 while (pos
< gb
.textsize
) {
590 if (ch
== '@' && gb
[pos
+1] == '@') { pos
+= 2; break; } // found
591 if (ch
> ' ' || ch
== '\n') { ++lidx
; continue lineloop
; } // next line
594 //conwriteln("found \"@@\" at line ", lidx);
595 // skip spaces after "@@"
596 while (pos
< gb
.textsize
) {
598 if (ch
== '\n') { ++lidx
; continue lineloop
; } // next line
602 if (pos
>= gb
.textsize
) break; // no more text
605 while (pos
< gb
.textsize
) {
607 if (ch
== '\n') break;
612 //conwriteln("got fname: '", fname, "'");
616 int ls
= lc
.linestart(lidx
);
617 int le
= lc
.linestart(lidx
+1);
618 if (ls
< le
) deleteText
!"start"(ls
, le
-ls
);
623 char[] getAttachName (char[] dest
, int lidx
) {
624 if (lidx
< 0 || lidx
>= lc
.linecount
) return null;
626 auto pos
= lc
.linestart(lidx
);
627 while (pos
< gb
.textsize
) {
629 if (ch
== '@' && gb
[pos
+1] == '@') { pos
+= 2; break; } // found
630 if (ch
> ' ' || ch
== '\n') return null; // not found
633 // skip spaces after "@@"
634 while (pos
< gb
.textsize
) {
636 if (ch
== '\n') return null; // not found
640 if (pos
>= gb
.textsize
) return null; // not found
643 while (pos
< gb
.textsize
) {
645 if (ch
== '\n') break;
646 if (dpos
>= dest
.length
) return null;
647 dest
.ptr
[dpos
++] = ch
;
650 return dest
.ptr
[0..dpos
];
653 // ah, who cares about speed?
654 void reformatFromLine(bool doUndoGroup
) (int lidx
) {
655 if (lidx
< 0) lidx
= 0;
656 if (lidx
>= lc
.linecount
) return;
658 static if (doUndoGroup
) {
659 bool undoGroupStarted
= false;
660 scope(exit
) if (undoGroupStarted
) undoGroupEnd();
662 void startUndoGroup () {
663 if (!undoGroupStarted
) {
664 undoGroupStarted
= true;
669 void startUndoGroup () {}
672 void normalizeQuoting (int lidx
) {
673 while (lidx
< lc
.linecount
) {
674 auto pos
= lc
.linestart(lidx
);
675 if (gb
[pos
] == '>') {
676 bool lastWasSpace
= false;
678 auto afterlastq
= pos
;
679 while (pos
< gb
.textsize
) {
681 if (ch
== '\n') break;
682 if (ch
== '>') { afterlastq
= ++pos
; continue; }
683 if (ch
== ' ') { ++pos
; continue; }
687 while (pos
< afterlastq
) {
690 if (ch
== ' ') { deleteText(pos
, 1); --afterlastq
; continue; } // remove space (thus normalizing quotes)
694 assert(pos
== afterlastq
);
695 if (pos
< gb
.textsize
&& gb
[pos
] > ' ') { startUndoGroup(); insertText
!("none", false)(pos
, " "); }
701 // try to join two lines, if it is possible; return `true` if succeed
702 bool tryJoinLines (int lidx
) {
704 if (lc
.linecount
== 1) return false; // nothing to do
705 if (lidx
+1 >= lc
.linecount
) return false; // nothing to do
706 auto ql0
= lineQuoteLevel(lidx
);
707 auto ql1
= lineQuoteLevel(lidx
+1);
708 if (ql0
!= ql1
) return false; // different quote levels, can't join
709 auto ls
= lc
.linestart(lidx
);
710 auto le
= lc
.lineend(lidx
);
711 if (le
-ls
< 1) return false; // wtf?!
712 if (gb
[le
-1] != ' ') return false; // no trailing space -- can't join
713 // don't join if next line is empty one: this is prolly paragraph delimiter
714 if (le
+1 >= gb
.textsize || gb
[le
+1] == '\n') return false;
715 if (gb
[le
+1] == '>') {
717 while (pp
< gb
.textsize
) {
719 if (ch
!= '>' && ch
!= ' ') break;
722 if (gb
[pp
-1] != ' ') return false;
727 // remove excessive spaces, if any
728 while (le
> ls
&& gb
[le
-1] == ' ') --le
;
729 assert(gb
[le
] == ' ');
730 ++le
; // but leave one space
731 while (le
< gb
.textsize
) {
732 if (gb
[le
] == '\n' || gb
[le
] > ' ') break;
737 assert(le
< gb
.textsize
&& gb
[le
] == '>');
738 while (le
< gb
.textsize
) {
740 if (ch
== '\n' ||
(ch
!= '>' && ch
!= ' ')) break;
747 // join lines; we'll split 'em later
748 for (int l
= lidx
; l
< lc
.linecount
; ) if (!tryJoinLines(l
)) ++l
;
750 // make quoting consistent
751 normalizeQuoting(lidx
);
753 void conwrlinerest (int pos
) {
754 if (pos
< 0 || pos
>= gb
.textsize
) return;
755 while (pos
< gb
.textsize
) {
757 if (ch
== '\n') break;
762 void conwrline (int lidx
) {
763 if (lidx
< 0 || lidx
>= lc
.linecount
) return;
764 conwrlinerest(lc
.linestart(lidx
));
767 // now split the lines; all lines are joined, so we can only split
768 lineloop
: while (lidx
< lc
.linecount
) {
769 auto ls
= lc
.linestart(lidx
);
770 auto le
= lc
.lineend(lidx
);
771 // calculate line length without trailing spaces
772 auto llen
= linelen(lidx
);
775 while (pe
> ls
&& gb
[pe
-1] <= ' ') { --pe
; --llen
; }
777 if (llen
<= 76) { ++lidx
; continue; } // nothing to do here
781 // skip quotes, if any
782 while (gb
[pos
] == '>') { ++curlen
; ++pos
; }
783 // skip leading spaces
784 while (gb
[pos
] != '\n' && gb
[pos
] <= ' ') { ++curlen
; ++pos
; }
785 if (pos
>= gb
.textsize || gb
[pos
] == '\n') { ++lidx
; continue; } // wtf?!
788 while (pos
< gb
.textsize
) {
789 immutable stpos
= pos
;
790 dchar ch
= dcharAtAdvance(pos
);
794 if (lwstart
>= 0 && curlen
> 76) {
797 insertText
!("none", false)(lwstart
, "\n");
800 while (gb
[ls
] == '>') {
801 insertText
!("none", false)(lwstart
, ">");
805 insertText
!("none", false)(lwstart
, " ");
810 if (ch
== '\n') break;
811 // not in word anymore
815 if (ch
== '\n') break;
816 if (ch
> ' ') { lwstart
= stpos
; inword
= true; }
825 // ////////////////////////////////////////////////////////////////////////// //
827 int level
; // quote level
828 int length
; // quote prefix length, in chars
831 QuoteInfo
calcQuote(T
:const(char)[]) (T s
) {
832 static if (is(T
== typeof(null))) {
836 if (s
.length
> 0 && s
[0] == '>') {
837 while (qi
.length
< s
.length
) {
838 if (s
[qi
.length
] != ' ') {
839 if (s
[qi
.length
] != '>') break;
844 if (s
.length
-qi
.length
> 1 && s
[qi
.length
] == ' ') ++qi
.length
;
851 // ////////////////////////////////////////////////////////////////////////// //
852 final class EditorWidget
: Widget
{
854 bool moveToBottom
= true;
856 this (SubWindow aparent
) {
858 editor
= new TextEditor(this, 0, 0, 10, 10);
861 void addText (const(char)[] s
) {
862 editor
.moveResize(x0
, y0
, (ww
< 1 ?
1 : ww
), (wh
< 1 ? wh
: 1));
863 editor
.doPasteStart();
864 scope(exit
) editor
.doPasteEnd();
865 editor
.doPutTextUtf(s
);
866 //editor.doPutChar('\n');
870 editor
.clearAndDisableUndo();
871 scope(exit
) editor
.reinstantiateUndo();
872 editor
.reformatFromLine
!false(0);
873 editor
.gotoXY(0, 0); // HACK
874 editor
.textChanged
= false;
877 string
[] extractAttaches () {
878 return editor
.extractAttaches();
881 private final void drawScrollBar () {
882 restoreClip(); // the easiest way again
883 gxDrawScrollBar(GxRect(x0
, y0
, 4, height
), editor
.linecount
-1, editor
.topline
+editor
.visibleLinesPerWindow
-1);
886 protected override void doPaint () {
887 if (ww
< 1 || wh
< 1) return;
888 editor
.moveResize(x0
+5, y0
, ww
-5*2, wh
-gxTextHeightUtf
);
890 if (moveToBottom
) { moveToBottom
= false; editor
.gotoXY(0, editor
.linecount
); } // HACK!
891 gxFillRect(x0
, y0
, ww
, wh
, gxRGB
!(0, 0, 120));
897 // return `true` if event was eaten
898 override bool onKey (KeyEvent event
) {
899 if (!active
) return false;
901 if (editor
.processKey(event
)) return true;
902 if (event
== "S-Insert") {
903 getClipboardText(vbwin
, delegate (in char[] text
) {
905 editor
.doPasteStart();
906 scope(exit
) editor
.doPasteEnd();
907 editor
.doPutTextUtf(text
[]);
912 if (event
== "C-Insert") {
913 auto mtr
= editor
.markedBlockRange();
916 brng
.reserve(mtr
.length
);
917 foreach (char ch
; mtr
) brng
~= ch
;
918 if (brng
.length
> 0) {
919 setClipboardText(vbwin
, cast(string
)brng
); // it is safe to cast here
920 setPrimarySelection(vbwin
, cast(string
)brng
); // it is safe to cast here
922 editor
.doBlockResetMark();
927 if (event
== "M-Tab") {
928 char[1024] abuf
= void;
929 auto attname
= editor
.getAttachName(abuf
[], editor
.cury
);
930 if (attname
.length
&& editor
.curx
>= editor
.linelen(editor
.cury
)) {
931 auto cplist
= buildAutoCompletion(attname
);
932 auto atnlen
= attname
.length
;
933 auto adg
= delegate (string s
) {
934 //conwriteln("attname=[", attname, "]; s=[", s, "]");
935 if (s
.length
<= atnlen
/*|| s[0..attname.length] != attname*/) return;
936 editor
.undoGroupStart();
937 scope(exit
) editor
.undoGroupEnd();
938 editor
.doPutTextUtf(s
[atnlen
..$]);
940 if (cplist
.length
== 1) {
943 auto acw
= new SelectCompletionWindow(attname
, cplist
, true);
944 acw
.onSelected
= adg
;
950 return super.onKey(event
);
953 override bool onMouse (MouseEvent event
) {
954 if (!active
) return false;
956 event
.mouse2xy(mx
, my
);
957 if (mx
>= x0
&& my
>= y0
&& mx
<= x1
&& my
<= y1
) {
958 if (editor
.processClick(mx
-x0
, my
-y0
, event
)) return true;
960 return super.onMouse(event
);
963 override bool onChar (dchar ch
) {
964 if (!active
) return false;
965 if (editor
.processChar(ch
)) return true;
966 return super.onChar(ch
);
971 // ////////////////////////////////////////////////////////////////////////// //
972 final class LineEditWidget
: Widget
{
975 public int titwdt
= -1;
977 this (SubWindow aparent
, string atitle
=null, int atitwdt
=-1) {
979 parent
.setupClientClip();
980 ww
= gxClipRect
.width
;
981 wh
= gxTextHeightUtf
+2;
984 if (titwdt
< 0) titwdt
= gxTextWidthUtf(atitle
)+2;
985 editor
= new TextEditor(this, 0, 0, ww
, wh
, true); // singleline
988 @property bool readonly () const nothrow { return editor
.readonly
; }
989 @property void readonly (bool v
) nothrow { editor
.readonly
= v
; }
991 @property bool killTextOnChar () const nothrow { return editor
.killTextOnChar
; }
992 @property void killTextOnChar (bool v
) nothrow { editor
.killTextOnChar
= v
; }
994 @property string
str () {
995 if (editor
.textsize
== 0) return null;
997 res
.reserve(editor
.textsize
);
998 foreach (char ch
; editor
[]) res
~= ch
;
999 return cast(string
)res
; // it is safe to cast here
1002 @property void str (const(char)[] s
) {
1003 editor
.clearAndDisableUndo();
1004 scope(exit
) editor
.reinstantiateUndo();
1006 editor
.doPutTextUtf(s
);
1007 editor
.textChanged
= false;
1010 protected override void doPaint () {
1011 if (ww
< 1 || wh
< 1) return;
1018 gxDrawTextUtf(cx0
+titwdt
-gxTextWidthUtf(title
)-1, cy0
+1, title
, gxRGB
!(255, 255, 255));
1020 gxClipRect
.intersect(GxRect(GxPoint(cx0
, y0
), GxPoint(x1
, y1
)));
1022 gxFillRect(cx0
+1, cy0
, ww
-titwdt
-2, wh
, gxRGB
!(0, 0, 0));
1023 gxClipRect
.intersect(GxRect(GxPoint(cx0
+2, y0
), GxPoint(x1
-2, y1
)));
1028 editor
.moveResize(cx0
, cy0
+1, cx1
-cx0
+1, gxTextHeightUtf
);
1033 // return `true` if event was eaten
1034 override bool onKey (KeyEvent event
) {
1035 if (!active
) return false;
1036 if (event
.pressed
) {
1037 if (editor
.processKey(event
)) return true;
1038 if (event
== "S-Insert") {
1039 getClipboardText(vbwin
, delegate (in char[] text
) {
1041 editor
.doPasteStart();
1042 scope(exit
) editor
.doPasteEnd();
1043 editor
.doPutTextUtf(text
[]);
1048 if (event
== "C-Insert") {
1049 auto mtr
= editor
.markedBlockRange();
1052 brng
.reserve(mtr
.length
);
1053 foreach (char ch
; mtr
) brng
~= ch
;
1054 if (brng
.length
> 0) {
1055 setClipboardText(vbwin
, cast(string
)brng
); // it is safe to cast here
1056 setPrimarySelection(vbwin
, cast(string
)brng
); // it is safe to cast here
1058 editor
.doBlockResetMark();
1063 return super.onKey(event
);
1066 override bool onMouse (MouseEvent event
) {
1067 if (!active
) return false;
1069 event
.mouse2xy(mx
, my
);
1070 if (mx
>= x0
&& my
>= y0
&& mx
<= x1
&& my
<= y1
) {
1071 if (editor
.processClick(mx
-x0
, my
-y0
, event
)) return true;
1073 return super.onMouse(event
);
1076 override bool onChar (dchar ch
) {
1077 if (!active
) return false;
1078 if (editor
.processChar(ch
)) return true;
1079 return super.onChar(ch
);