egra: better selectors (they can be customised with return type now, and has proper...
[iv.d.git] / egra / gui / editor.d
blobf647ae84552feeb470aa0dcf50db1fdbd75e82a7
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 iv.egra.gui.editor /*is aliced*/;
18 import arsd.simpledisplay;
20 import iv.alice;
21 import iv.cmdcon;
22 import iv.dynstring;
23 import iv.strex;
24 import iv.utfutil;
25 import iv.vfs;
27 import iv.egeditor.editor;
28 //import iv.egeditor.highlighters;
30 import iv.egra.gfx;
32 import iv.egra.gui.subwindows;
33 import iv.egra.gui.widgets;
34 import iv.egra.gui.dialogs;
37 // ////////////////////////////////////////////////////////////////////////// //
38 final class ChiTextMeter : EgTextMeter {
39 GxKerning twkern;
41 //int currofs; /// x offset for current char (i.e. the last char that was passed to `advance()` should be drawn with this offset)
42 //int currwdt; /// current line width (including, the last char that was passed to `advance()`), preferably without trailing empty space between chars
43 //int currheight; /// current text height; keep this in sync with the current state; `reset` should set it to "default text height"
45 /// this should reset text width iterator (and curr* fields); tabsize > 0: process tabs as... well... tabs ;-)
46 override void reset (int tabsize) nothrow {
47 twkern.reset(tabsize);
48 currheight = gxTextHeightUtf;
51 /// advance text width iterator, return x position for drawing next char
52 override void advance (dchar ch, in ref GapBuffer.HighState hs) nothrow {
53 twkern.fixWidthPre(ch);
54 currofs = twkern.currOfs;
55 currwdt = twkern.nextOfsNoSpacing;
58 /// finish text iterator; it should NOT reset curr* fields!
59 /// WARNING: EditorEngine tries to call this after each `reset()`, but user code may not
60 override void finish () nothrow {
61 //return twkern.finalWidth;
66 // ////////////////////////////////////////////////////////////////////////// //
67 final class TextEditor : EditorEngine {
68 enum TEDSingleOnly; // only for single-line mode
69 enum TEDMultiOnly; // only for multiline mode
70 enum TEDEditOnly; // only for non-readonly mode
71 enum TEDROOnly; // only for readonly mode
73 static struct TEDKey { string key; string help; bool hidden; } // UDA
75 static string TEDImplX(string key, string help, string code, size_t ln) () {
76 static assert(key.length > 0, "wtf?!");
77 static assert(code.length > 0, "wtf?!");
78 string res = "@TEDKey("~key.stringof~", "~help.stringof~") void _ted_";
79 int pos = 0;
80 while (pos < key.length) {
81 char ch = key[pos++];
82 if (key.length-pos > 0 && key[pos] == '-') {
83 if (ch == 'C' || ch == 'c') { ++pos; res ~= "Ctrl"; continue; }
84 if (ch == 'M' || ch == 'm') { ++pos; res ~= "Alt"; continue; }
85 if (ch == 'S' || ch == 's') { ++pos; res ~= "Shift"; continue; }
87 if (ch == '^') { res ~= "Ctrl"; continue; }
88 if (ch >= 'a' && ch <= 'z') ch -= 32;
89 if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || ch == '_') res ~= ch; else res ~= '_';
91 res ~= ln.stringof;
92 res ~= " () {"~code~"}";
93 return res;
96 mixin template TEDImpl(string key, string help, string code, size_t ln=__LINE__) {
97 mixin(TEDImplX!(key, help, code, ln));
100 mixin template TEDImpl(string key, string code, size_t ln=__LINE__) {
101 mixin(TEDImplX!(key, "", code, ln));
104 protected:
105 KeyEvent[32] comboBuf;
106 int comboCount; // number of items in `comboBuf`
107 Widget pw;
109 ChiTextMeter chiTextMeter;
111 public:
112 this (Widget apw, int x0, int y0, int w, int h, bool asinglesine=false) {
113 pw = apw;
114 //coordsInPixels = true;
115 lineHeightPixels = gxTextHeightUtf;
116 chiTextMeter = new ChiTextMeter();
117 super(x0, y0, w, h, null, asinglesine);
118 textMeter = chiTextMeter;
119 utfuck = true;
120 visualtabs = true;
121 tabsize = 4; // spaces
124 final int lineQuoteLevel (int lidx) {
125 if (lidx < 0 || lidx >= lc.linecount) return 0;
126 int pos = lc.line2pos(lidx);
127 auto ts = gb.textsize;
128 if (pos >= ts) return 0;
129 if (gb[pos] != '>') return 0;
130 int count = 1;
131 ++pos;
132 while (pos < ts) {
133 char ch = gb[pos++];
134 if (ch == '>') ++count;
135 else if (ch == '\n') break;
136 else if (ch != ' ') break;
138 return count;
141 public override void drawCursor () {
142 if (pw.isFocused) {
143 int lcx, lcy;
144 localCursorXY(&lcx, &lcy);
145 //drawTextCursor(/*pw.parent.isFocused*/true, x0+lcx, y0+lcy);
146 pw.drawTextCursor(x0+lcx, y0+lcy, gxTextHeightUtf);
150 // not here, 'cause this is done before text
151 public override void drawStatus () {}
153 public final paintStatusLine () {
154 import core.stdc.stdio : snprintf;
155 int sx = x0, sy = y0+height;
156 gxFillRect(sx, sy, width, gxTextHeightUtf, pw.getColor("status-back"));
157 char[128] buf = void;
158 auto len = snprintf(buf.ptr, buf.length, "%04d:%04d %d", curx, cury, linecount);
159 gxDrawTextUtf(sx+2, sy, buf[0..len], pw.getColor("status-text"));
162 public override void drawPage () {
163 fullDirty(); // HACK!
164 gxWithSavedClip{
165 gxClipRect.intersect(GxRect(x0, y0, width, height));
166 super.drawPage();
168 if (!singleline) paintStatusLine();
172 public override void drawLine (int lidx, int yofs, int xskip) {
173 int x = x0-xskip;
174 int y = y0+yofs;
175 auto pos = lc.line2pos(lidx);
176 auto lea = lc.line2pos(lidx+1);
177 auto ts = gb.textsize;
178 bool utfucked = utfuck;
179 immutable int bs = bstart, be = bend;
180 immutable ls = pos;
182 uint clr = pw.getColor("text");
183 immutable uint markFgClr = pw.getColor("mark-text");
184 immutable uint markBgClr = pw.getColor("mark-back");
186 if (!singleline) {
187 int qlevel = lineQuoteLevel(lidx);
188 if (qlevel) {
189 final switch (qlevel%2) {
190 case 0: clr = pw.getColor("quote0-text"); break;
191 case 1: clr = pw.getColor("quote1-text"); break;
193 } else {
194 char[1024] abuf = void;
195 auto aname = getAttachName(abuf[], lidx);
196 if (aname.length) {
197 try {
198 import std.file : exists, isFile;
199 clr = (aname.exists && aname.isFile ? pw.getColor("attach-file-text") : pw.getColor("attach-bad-text"));
200 } catch (Exception e) {}
205 bool checkMarking = false;
206 if (hasMarkedBlock) {
207 //conwriteln("bs=", bs, "; be=", be, "; pos=", pos, "; lea=", lea);
208 if (pos >= bs && lea <= be) {
209 // full line
210 gxFillRect(x0, y, winw, lineHeightPixels, markBgClr);
211 clr = markFgClr;
212 } else if (pos < be && lea > bs) {
213 // draw block background
214 auto tpos = pos;
215 int bx0 = x, bx1 = x;
216 chiTextMeter.twkern.reset(visualtabs ? tabsize : 0);
217 while (tpos < lea) {
218 immutable dchar dch = dcharAtAdvance(tpos);
219 if (!singleline && dch == '\n') break;
220 chiTextMeter.twkern.fixWidthPre(dch);
221 if (tpos > bs) break;
223 bx0 = bx1 = x+chiTextMeter.twkern.currOfs;
224 bool eolhit = false;
225 while (tpos < lea) {
226 if (tpos >= be) break;
227 immutable dchar dch = dcharAtAdvance(tpos);
228 if (!singleline && dch == '\n') { eolhit = (tpos < be); break; }
229 chiTextMeter.twkern.fixWidthPre(dch);
230 if (tpos > be) break;
232 bx1 = (eolhit ? x0+width : x+chiTextMeter.twkern.finalWidth);
233 gxFillRect(bx0, y, bx1-bx0+1, lineHeightPixels, markBgClr);
234 checkMarking = true;
237 if (singleline && hasMarkedBlock) checkMarking = true; // let it be
239 //twkern.reset(visualtabs ? tabsize : 0);
240 uint cc = clr;
242 int epos = pos;
243 if (singleline) {
244 epos = ts;
245 } else {
246 while (epos < ts) if (gb[epos++] == '\n') { --epos; break; }
249 //FIXME: tabsize
250 int wdt = gxDrawTextUtf(GxDrawTextOptions.Tab(tabsize), x, y, this[pos..epos], delegate (in ref state) nothrow @trusted {
251 return (checkMarking ? (pos+state.spos >= bs && pos+state.epos <= be ? markFgClr : clr) : clr);
253 if (!singleline) {
254 if (epos > pos && gb[epos-1] == ' ') {
255 gxDrawChar(x+wdt, y, '\u2248', pw.getColor("wrap-mark-text"));
260 while (pos < ts) {
261 if (checkMarking) cc = (pos >= bs && pos < be ? markFgClr : clr);
262 immutable dchar dch = dcharAtAdvance(pos);
263 if (!singleline && dch == '\n') {
264 // draw "can wrap" mark
265 if (pos-2 >= ls && gb[pos-2] == ' ') {
266 int xx = x+twkern.finalWidth+1;
267 gxDrawChar(xx, y, '\n', gxRGB!(0, 0, 220));
269 break;
271 if (dch == '\t' && visualtabs) {
272 int xx = x+twkern.fixWidthPre('\t');
273 gxHLine(xx, y+lineHeightPixels/2, twkern.tablength, gxRGB!(200, 0, 0));
274 } else {
275 gxDrawChar(x+twkern.fixWidthPre(dch), y, dch, cc);
281 // just clear line
282 // use `winXXX` vars to know window dimensions
283 public override void drawEmptyLine (int yofs) {
284 // nothing to do here
287 protected enum Ecc { None, Eaten, Combo }
289 // None: not valid
290 // Eaten: exact hit
291 // Combo: combo start
292 // comboBuf should contain comboCount keys!
293 protected final Ecc checkKeys (const(char)[] keys) {
294 foreach (immutable cidx; 0..comboCount+1) {
295 keys = keys.xstrip;
296 if (keys.length == 0) return Ecc.Combo;
297 usize kepos = 0;
298 while (kepos < keys.length && keys.ptr[kepos] > ' ') ++kepos;
299 if (comboBuf[cidx] != keys[0..kepos]) return Ecc.None;
300 keys = keys[kepos..$];
302 return (keys.xstrip.length ? Ecc.Combo : Ecc.Eaten);
305 // fuck! `(this ME)` trick doesn't work here
306 protected final Ecc doEditorCommandByUDA(ME=typeof(this)) (KeyEvent key) {
307 import std.traits;
308 bool possibleCombo = false;
309 // temporarily add current key to combo
310 comboBuf[comboCount] = key;
311 // check all known combos
312 foreach (string memn; __traits(allMembers, ME)) {
313 static if (is(typeof(&__traits(getMember, ME, memn)))) {
314 import std.meta : AliasSeq;
315 alias mx = AliasSeq!(__traits(getMember, ME, memn))[0];
316 static if (isCallable!mx && hasUDA!(mx, TEDKey)) {
317 // check modifiers
318 bool goodMode = true;
319 static if (hasUDA!(mx, TEDSingleOnly)) { if (!singleline) goodMode = false; }
320 static if (hasUDA!(mx, TEDMultiOnly)) { if (singleline) goodMode = false; }
321 static if (hasUDA!(mx, TEDEditOnly)) { if (readonly) goodMode = false; }
322 static if (hasUDA!(mx, TEDROOnly)) { if (!readonly) goodMode = false; }
323 if (goodMode) {
324 foreach (const TEDKey attr; getUDAs!(mx, TEDKey)) {
325 auto cc = checkKeys(attr.key);
326 if (cc == Ecc.Eaten) {
327 // hit
328 static if (is(ReturnType!mx == void)) {
329 comboCount = 0; // reset combo
330 mx();
331 return Ecc.Eaten;
332 } else {
333 if (mx()) {
334 comboCount = 0; // reset combo
335 return Ecc.Eaten;
338 } else if (cc == Ecc.Combo) {
339 possibleCombo = true;
346 // check if we can start/continue combo
347 if (possibleCombo) {
348 if (++comboCount < comboBuf.length-1) return Ecc.Combo;
350 // if we have combo prefix, eat key unconditionally
351 if (comboCount > 0) {
352 comboCount = 0; // reset combo, too long, or invalid, or none
353 return Ecc.Eaten;
355 return Ecc.None;
358 bool processKey (KeyEvent key) {
359 final switch (doEditorCommandByUDA(key)) {
360 case Ecc.None: break;
361 case Ecc.Combo:
362 case Ecc.Eaten:
363 return true;
366 return false;
369 bool processChar (dchar ch) {
370 if (ch < ' ' || ch == 127) return false;
371 if (ch == ' ') {
372 // check if we should reformat
373 auto llen = linelen(cury);
374 if (llen >= 76) {
375 if (curx == 0 || gb[curpos-1] != ' ') doPutChar(' ');
376 auto ocx = curx;
377 auto ocy = cury;
378 // normalize ocx
380 int rpos = lc.linestart(ocy);
381 int qcnt = 0;
382 if (gb[rpos] == '>') {
383 while (gb[rpos] == '>') { ++qcnt; ++rpos; }
384 if (gb[rpos] == ' ') { ++qcnt; ++rpos; }
386 //conwriteln("origcx=", ocx, "; ncx=", ocx-qcnt, "; qcnt=", qcnt);
387 if (ocx < qcnt) ocx = qcnt; else ocx -= qcnt;
389 reformatFromLine!true(cury);
390 // position cursor
391 while (ocy < lc.linecount) {
392 int rpos = lc.linestart(ocy);
393 llen = linelen(ocy);
394 //conwriteln(" 00: ocy=", ocy, "; llen=", llen, "; ocx=", ocx);
395 int qcnt = 0;
396 if (gb[rpos] == '>') {
397 while (gb[rpos] == '>') { --llen; ++qcnt; ++rpos; }
398 if (gb[rpos] == ' ') { --llen; ++qcnt; ++rpos; }
400 //conwriteln(" 01: ocy=", ocy, "; llen=", llen, "; ocx=", ocx, "; qcnt=", qcnt);
401 if (ocx <= llen) { ocx += qcnt; break; }
402 ocx -= llen;
403 ++ocy;
405 gotoXY(ocx, ocy);
406 return true;
409 doPutDChar(ch);
410 return true;
413 bool processClick (int x, int y, MouseEvent event) {
414 if (x < 0 || y < 0 || x >= winw || y >= winh) return false;
415 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
416 int tx, ty;
417 widget2text(x, y, tx, ty);
418 gotoXY(tx, ty);
419 return true;
421 return false;
424 final:
425 void processWordWith (scope char delegate (char ch) dg) {
426 if (dg is null) return;
427 bool undoAdded = false;
428 scope(exit) if (undoAdded) undoGroupEnd();
429 auto pos = curpos;
430 if (!isWordChar(gb[pos])) return;
431 // find word start
432 while (pos > 0 && isWordChar(gb[pos-1])) --pos;
433 while (pos < gb.textsize) {
434 auto ch = gb[pos];
435 if (!isWordChar(gb[pos])) break;
436 auto nc = dg(ch);
437 if (ch != nc) {
438 if (!undoAdded) { undoAdded = true; undoGroupStart(); }
439 replaceText!"none"(pos, 1, (&nc)[0..1]);
441 ++pos;
443 gotoPos(pos);
446 final:
447 @TEDMultiOnly mixin TEDImpl!("Up", q{ doUp(); });
448 @TEDMultiOnly mixin TEDImpl!("S-Up", q{ doUp(true); });
449 @TEDMultiOnly mixin TEDImpl!("C-Up", q{ doScrollUp(); });
450 @TEDMultiOnly mixin TEDImpl!("S-C-Up", q{ doScrollUp(true); });
452 @TEDMultiOnly mixin TEDImpl!("Down", q{ doDown(); });
453 @TEDMultiOnly mixin TEDImpl!("S-Down", q{ doDown(true); });
454 @TEDMultiOnly mixin TEDImpl!("C-Down", q{ doScrollDown(); });
455 @TEDMultiOnly mixin TEDImpl!("S-C-Down", q{ doScrollDown(true); });
457 mixin TEDImpl!("Left", q{ doLeft(); });
458 mixin TEDImpl!("S-Left", q{ doLeft(true); });
459 mixin TEDImpl!("C-Left", q{ doWordLeft(); });
460 mixin TEDImpl!("S-C-Left", q{ doWordLeft(true); });
462 mixin TEDImpl!("Right", q{ doRight(); });
463 mixin TEDImpl!("S-Right", q{ doRight(true); });
464 mixin TEDImpl!("C-Right", q{ doWordRight(); });
465 mixin TEDImpl!("S-C-Right", q{ doWordRight(true); });
467 @TEDMultiOnly mixin TEDImpl!("PageUp", q{ doPageUp(); });
468 @TEDMultiOnly mixin TEDImpl!("S-PageUp", q{ doPageUp(true); });
469 @TEDMultiOnly mixin TEDImpl!("C-PageUp", q{ doTextTop(); });
470 @TEDMultiOnly mixin TEDImpl!("S-C-PageUp", q{ doTextTop(true); });
472 @TEDMultiOnly mixin TEDImpl!("PageDown", q{ doPageDown(); });
473 @TEDMultiOnly mixin TEDImpl!("S-PageDown", q{ doPageDown(true); });
474 @TEDMultiOnly mixin TEDImpl!("C-PageDown", q{ doTextBottom(); });
475 @TEDMultiOnly mixin TEDImpl!("S-C-PageDown", q{ doTextBottom(true); });
477 mixin TEDImpl!("Home", q{ doHome(); });
478 mixin TEDImpl!("S-Home", q{ doHome(true, true); });
479 @TEDMultiOnly mixin TEDImpl!("C-Home", q{ doPageTop(); });
480 @TEDMultiOnly mixin TEDImpl!("S-C-Home", q{ doPageTop(true); });
482 mixin TEDImpl!("End", q{ doEnd(); });
483 mixin TEDImpl!("S-End", q{ doEnd(true); });
484 @TEDMultiOnly mixin TEDImpl!("C-End", q{ doPageBottom(); });
485 @TEDMultiOnly mixin TEDImpl!("S-C-End", q{ doPageBottom(true); });
487 @TEDEditOnly mixin TEDImpl!("Backspace", q{ doBackspace(); });
488 @TEDSingleOnly @TEDEditOnly mixin TEDImpl!("M-Backspace", "delete previous word", q{ doDeleteWord(); });
489 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("M-Backspace", "delete previous word or unindent", q{ doBackByIndent(); });
491 mixin TEDImpl!("Delete", q{
492 doDelete();
495 //mixin TEDImpl!("^Insert", "copy block to clipboard file, reset block mark", q{ if (tempBlockFileName.length == 0) return; doBlockWrite(tempBlockFileName); doBlockResetMark(); });
497 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("Enter", q{
498 auto ly = cury;
499 if (lineQuoteLevel(ly)) {
500 doLineSplit(false); // no autoindent
501 auto ls = lc.linestart(ly);
502 undoGroupStart();
503 scope(exit) undoGroupEnd();
504 while (gb[ls] == '>') {
505 ++ls;
506 insertText!"end"(curpos, ">");
508 if (gb[curpos] > ' ') insertText!"end"(curpos, " ");
509 } else {
510 doLineSplit();
513 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("M-Enter", "split line without autoindenting", q{ doLineSplit(false); });
516 mixin TEDImpl!("F3", "start/stop/reset block marking", q{ doToggleBlockMarkMode(); });
517 mixin TEDImpl!("C-F3", "reset block mark", q{ doBlockResetMark(); });
518 @TEDEditOnly mixin TEDImpl!("F5", "copy block", q{ doBlockCopy(); });
519 // mixin TEDImpl!("^F5", "copy block to clipboard file", q{ if (tempBlockFileName.length == 0) return; doBlockWrite(tempBlockFileName); });
520 //@TEDEditOnly mixin TEDImpl!("S-F5", "insert block from clipboard file", q{ if (tempBlockFileName.length == 0) return; waitingInF5 = true; });
521 @TEDEditOnly mixin TEDImpl!("F6", "move block", q{ doBlockMove(); });
522 @TEDEditOnly mixin TEDImpl!("F8", "delete block", q{ doBlockDelete(); });
524 mixin TEDImpl!("C-A", "move to line start", q{ doHome(); });
525 mixin TEDImpl!("C-E", "move to line end", q{ doEnd(); });
527 @TEDMultiOnly mixin TEDImpl!("M-I", "jump to previous bookmark", q{ doBookmarkJumpUp(); });
528 @TEDMultiOnly mixin TEDImpl!("M-J", "jump to next bookmark", q{ doBookmarkJumpDown(); });
529 @TEDMultiOnly mixin TEDImpl!("M-K", "toggle bookmark", q{ doBookmarkToggle(); });
531 @TEDEditOnly mixin TEDImpl!("M-C", "capitalize word", q{
532 bool first = true;
533 processWordWith((char ch) {
534 if (first) { first = false; ch = ch.toupper; }
535 return ch;
538 @TEDEditOnly mixin TEDImpl!("M-Q", "lowercase word", q{ processWordWith((char ch) => ch.tolower); });
539 @TEDEditOnly mixin TEDImpl!("M-U", "uppercase word", q{ processWordWith((char ch) => ch.toupper); });
541 @TEDMultiOnly mixin TEDImpl!("M-S-L", "force center current line", q{ makeCurLineVisibleCentered(true); });
542 @TEDEditOnly mixin TEDImpl!("C-U", "undo", q{ doUndo(); });
543 @TEDEditOnly mixin TEDImpl!("M-S-U", "redo", q{ doRedo(); });
544 @TEDEditOnly mixin TEDImpl!("C-W", "remove previous word", q{ doDeleteWord(); });
545 @TEDEditOnly mixin TEDImpl!("C-Y", "remove current line", q{ doKillLine(); });
547 //@TEDMultiOnly @TEDEditOnly mixin TEDImpl!("Tab", q{ doPutText(" "); });
548 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-Tab", "indent block", q{ doIndentBlock(); });
549 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-S-Tab", "unindent block", q{ doUnindentBlock(); });
551 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-I", "indent block", q{ doIndentBlock(); });
552 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-U", "unindent block", q{ doUnindentBlock(); });
553 @TEDEditOnly mixin TEDImpl!("C-K C-E", "clear from cursor to EOL", q{ doKillToEOL(); });
554 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K Tab", "indent block", q{ doIndentBlock(); });
555 // @TEDEditOnly mixin TEDImpl!("^K M-Tab", "untabify", q{ doUntabify(gb.tabsize ? gb.tabsize : 2); }); // alt+tab: untabify
556 // @TEDEditOnly mixin TEDImpl!("^K C-space", "remove trailing spaces", q{ doRemoveTailingSpaces(); });
557 // mixin TEDImpl!("C-K C-T", /*"toggle \"visual tabs\" mode",*/ q{ visualtabs = !visualtabs; });
559 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-B", q{ doSetBlockStart(); });
560 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-K", q{ doSetBlockEnd(); });
562 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-C", q{ doBlockCopy(); });
563 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-M", q{ doBlockMove(); });
564 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-Y", q{ doBlockDelete(); });
565 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-H", q{ doBlockResetMark(); });
566 // fuckin' vt100!
567 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K Backspace", q{ doBlockResetMark(); });
569 @TEDEditOnly mixin TEDImpl!("C-Q Tab", q{ doPutChar('\t'); });
570 //mixin TEDImpl!("C-Q C-U", "toggle utfuck mode", q{ utfuck = !utfuck; }); // ^Q^U: switch utfuck mode
571 //mixin TEDImpl!("C-Q 1", "switch to koi8", q{ utfuck = false; codepage = CodePage.koi8u; fullDirty(); });
572 //mixin TEDImpl!("C-Q 2", "switch to cp1251", q{ utfuck = false; codepage = CodePage.cp1251; fullDirty(); });
573 //mixin TEDImpl!("C-Q 3", "switch to cp866", q{ utfuck = false; codepage = CodePage.cp866; fullDirty(); });
574 mixin TEDImpl!("C-Q C-B", "go to block start", q{ if (hasMarkedBlock) gotoPos!true(bstart); lastBGEnd = false; });
576 @TEDSingleOnly @TEDEditOnly mixin TEDImpl!("C-Q Enter", "intert LF", q{ doPutChar('\n'); });
577 @TEDSingleOnly @TEDEditOnly mixin TEDImpl!("C-Q M-Enter", "intert CR", q{ doPutChar('\r'); });
579 mixin TEDImpl!("C-Q C-K", "go to block end", q{ if (hasMarkedBlock) gotoPos!true(bend); lastBGEnd = true; });
581 @TEDMultiOnly @TEDROOnly mixin TEDImpl!("Space", q{ doPageDown(); });
582 @TEDMultiOnly @TEDROOnly mixin TEDImpl!("S-Space", q{ doPageUp(); });
584 final:
585 dynstring[] extractAttaches () {
586 dynstring[] res;
587 int lidx = 0;
588 lineloop: while (lidx < lc.linecount) {
589 // find "@@"
590 auto pos = lc.linestart(lidx);
591 //conwriteln("checking line ", lidx, ": '", gb[pos], "'");
592 while (pos < gb.textsize) {
593 char ch = gb[pos];
594 if (ch == '@' && gb[pos+1] == '@') { pos += 2; break; } // found
595 if (ch > ' ' || ch == '\n') { ++lidx; continue lineloop; } // next line
596 ++pos;
598 //conwriteln("found \"@@\" at line ", lidx);
599 // skip spaces after "@@"
600 while (pos < gb.textsize) {
601 char ch = gb[pos];
602 if (ch == '\n') { ++lidx; continue lineloop; } // next line
603 if (ch > ' ') break;
604 ++pos;
606 if (pos >= gb.textsize) break; // no more text
607 // extract file name
608 dynstring fname;
609 while (pos < gb.textsize) {
610 char ch = gb[pos];
611 if (ch == '\n') break;
612 fname ~= ch;
613 ++pos;
615 if (fname.length) {
616 //conwriteln("got fname: '", fname, "'");
617 res ~= fname;
619 // remove this line
620 int ls = lc.linestart(lidx);
621 int le = lc.linestart(lidx+1);
622 if (ls < le) deleteText!"start"(ls, le-ls);
624 return res;
627 char[] getAttachName (char[] dest, int lidx) {
628 if (lidx < 0 || lidx >= lc.linecount) return null;
629 // find "@@"
630 auto pos = lc.linestart(lidx);
631 while (pos < gb.textsize) {
632 char ch = gb[pos];
633 if (ch == '@' && gb[pos+1] == '@') { pos += 2; break; } // found
634 if (ch > ' ' || ch == '\n') return null; // not found
635 ++pos;
637 // skip spaces after "@@"
638 while (pos < gb.textsize) {
639 char ch = gb[pos];
640 if (ch == '\n') return null; // not found
641 if (ch > ' ') break;
642 ++pos;
644 if (pos >= gb.textsize) return null; // not found
645 // extract file name
646 usize dpos = 0;
647 while (pos < gb.textsize) {
648 char ch = gb[pos];
649 if (ch == '\n') break;
650 if (dpos >= dest.length) return null;
651 dest.ptr[dpos++] = ch;
652 ++pos;
654 return dest.ptr[0..dpos];
657 // ah, who cares about speed?
658 void reformatFromLine(bool doUndoGroup) (int lidx) {
659 if (lidx < 0) lidx = 0;
660 if (lidx >= lc.linecount) return;
662 static if (doUndoGroup) {
663 bool undoGroupStarted = false;
664 scope(exit) if (undoGroupStarted) undoGroupEnd();
666 void startUndoGroup () {
667 if (!undoGroupStarted) {
668 undoGroupStarted = true;
669 undoGroupStart();
672 } else {
673 void startUndoGroup () {}
676 void normalizeQuoting (int lidx) {
677 while (lidx < lc.linecount) {
678 auto pos = lc.linestart(lidx);
679 if (gb[pos] == '>') {
680 bool lastWasSpace = false;
681 auto stpos = pos;
682 auto afterlastq = pos;
683 while (pos < gb.textsize) {
684 char ch = gb[pos];
685 if (ch == '\n') break;
686 if (ch == '>') { afterlastq = ++pos; continue; }
687 if (ch == ' ') { ++pos; continue; }
688 break;
690 pos = stpos;
691 while (pos < afterlastq) {
692 char ch = gb[pos];
693 assert(ch != '\n');
694 if (ch == ' ') { deleteText(pos, 1); --afterlastq; continue; } // remove space (thus normalizing quotes)
695 assert(ch == '>');
696 ++pos;
698 assert(pos == afterlastq);
699 if (pos < gb.textsize && gb[pos] > ' ') { startUndoGroup(); insertText!("none", false)(pos, " "); }
701 ++lidx;
705 // try to join two lines, if it is possible; return `true` if succeed
706 bool tryJoinLines (int lidx) {
707 assert(lidx >= 0);
708 if (lc.linecount == 1) return false; // nothing to do
709 if (lidx+1 >= lc.linecount) return false; // nothing to do
710 auto ql0 = lineQuoteLevel(lidx);
711 auto ql1 = lineQuoteLevel(lidx+1);
712 if (ql0 != ql1) return false; // different quote levels, can't join
713 auto ls = lc.linestart(lidx);
714 auto le = lc.lineend(lidx);
715 if (le-ls < 1) return false; // wtf?!
716 if (gb[le-1] != ' ') return false; // no trailing space -- can't join
717 // don't join if next line is empty one: this is prolly paragraph delimiter
718 if (le+1 >= gb.textsize || gb[le+1] == '\n') return false;
719 if (gb[le+1] == '>') {
720 int pp = le+1;
721 while (pp < gb.textsize) {
722 char ch = gb[pp];
723 if (ch != '>' && ch != ' ') break;
724 ++pp;
726 if (gb[pp-1] != ' ') return false;
728 // remove newline
729 startUndoGroup();
730 deleteText(le, 1);
731 // remove excessive spaces, if any
732 while (le > ls && gb[le-1] == ' ') --le;
733 assert(gb[le] == ' ');
734 ++le; // but leave one space
735 while (le < gb.textsize) {
736 if (gb[le] == '\n' || gb[le] > ' ') break;
737 deleteText(le, 1);
739 // remove quoting
740 if (ql0) {
741 assert(le < gb.textsize && gb[le] == '>');
742 while (le < gb.textsize) {
743 char ch = gb[le];
744 if (ch == '\n' || (ch != '>' && ch != ' ')) break;
745 deleteText(le, 1);
748 return true; // yay
751 // join lines; we'll split 'em later
752 for (int l = lidx; l < lc.linecount; ) if (!tryJoinLines(l)) ++l;
754 // make quoting consistent
755 normalizeQuoting(lidx);
757 void conwrlinerest (int pos) {
758 if (pos < 0 || pos >= gb.textsize) return;
759 while (pos < gb.textsize) {
760 char ch = gb[pos++];
761 if (ch == '\n') break;
762 conwrite(ch);
766 void conwrline (int lidx) {
767 if (lidx < 0 || lidx >= lc.linecount) return;
768 conwrlinerest(lc.linestart(lidx));
771 // now split the lines; all lines are joined, so we can only split
772 lineloop: while (lidx < lc.linecount) {
773 auto ls = lc.linestart(lidx);
774 auto le = lc.lineend(lidx);
775 // calculate line length without trailing spaces
776 auto llen = linelen(lidx);
778 auto pe = le;
779 while (pe > ls && gb[pe-1] <= ' ') { --pe; --llen; }
781 if (llen <= 76) { ++lidx; continue; } // nothing to do here
782 // need to wrap it
783 auto pos = ls;
784 int curlen = 0;
785 // skip quotes, if any
786 while (gb[pos] == '>') { ++curlen; ++pos; }
787 // skip leading spaces
788 while (gb[pos] != '\n' && gb[pos] <= ' ') { ++curlen; ++pos; }
789 if (pos >= gb.textsize || gb[pos] == '\n') { ++lidx; continue; } // wtf?!
790 int lwstart = -1;
791 bool inword = true;
792 while (pos < gb.textsize) {
793 immutable stpos = pos;
794 dchar ch = dcharAtAdvance(pos);
795 ++curlen;
796 if (inword) {
797 if (ch <= ' ') {
798 if (lwstart >= 0 && curlen > 76) {
799 // wrap
800 startUndoGroup();
801 insertText!("none", false)(lwstart, "\n");
802 ++lwstart;
803 if (gb[ls] == '>') {
804 while (gb[ls] == '>') {
805 insertText!("none", false)(lwstart, ">");
806 ++lwstart;
807 ++ls;
809 insertText!("none", false)(lwstart, " ");
811 ++lidx;
812 continue lineloop;
814 if (ch == '\n') break;
815 // not in word anymore
816 inword = false;
818 } else {
819 if (ch == '\n') break;
820 if (ch > ' ') { lwstart = stpos; inword = true; }
823 ++lidx;
829 // ////////////////////////////////////////////////////////////////////////// //
830 struct QuoteInfo {
831 int level; // quote level
832 int length; // quote prefix length, in chars
835 QuoteInfo calcQuote(T:const(char)[]) (T s) {
836 static if (is(T == typeof(null))) {
837 return QuoteInfo();
838 } else {
839 QuoteInfo qi;
840 if (s.length > 0 && s[0] == '>') {
841 while (qi.length < s.length) {
842 if (s[qi.length] != ' ') {
843 if (s[qi.length] != '>') break;
844 ++qi.level;
846 ++qi.length;
848 if (s.length-qi.length > 1 && s[qi.length] == ' ') ++qi.length;
850 return qi;
855 // ////////////////////////////////////////////////////////////////////////// //
856 final class EditorWidget : Widget {
857 TextEditor editor;
858 bool moveToBottom = true;
860 this (Widget aparent) {
861 tabStop = true;
862 super(aparent);
863 editor = new TextEditor(this, 0, 0, 10, 10);
866 this () {
867 assert(creatorCurrentParent !is null);
868 this(creatorCurrentParent);
871 void addText (const(char)[] s) {
872 immutable GxRect grect = globalRect;
873 editor.moveResize(grect.x0, grect.y0, (grect.width < 1 ? 1 : grect.width), (grect.height < 1 ? grect.height : 1));
874 editor.doPasteStart();
875 scope(exit) editor.doPasteEnd();
876 editor.doPutTextUtf(s);
877 //editor.doPutChar('\n');
880 void reformat () {
881 editor.clearAndDisableUndo();
882 scope(exit) editor.reinstantiateUndo();
883 editor.reformatFromLine!false(0);
884 editor.gotoXY(0, 0); // HACK
885 editor.textChanged = false;
888 dynstring[] extractAttaches () {
889 return editor.extractAttaches();
892 private final void drawScrollBar () {
893 //restoreClip(); // the easiest way again
894 immutable GxRect grect = globalRect;
895 gxDrawScrollBar(GxRect(grect.x0, grect.y0, 4, height), editor.linecount-1, editor.topline+editor.visibleLinesPerWindow-1);
898 protected override void doPaint (GxRect grect) {
899 if (width < 1 || height < 1) return;
900 editor.moveResize(grect.x0+5, grect.y0, grect.width-5*2, grect.height-gxTextHeightUtf);
902 if (moveToBottom) { moveToBottom = false; editor.gotoXY(0, editor.linecount); } // HACK!
903 gxFillRect(grect, getColor("back"));
904 gxWithSavedClip {
905 editor.drawPage();
908 drawScrollBar();
911 // return `true` if event was eaten
912 override bool onKey (KeyEvent event) {
913 if (!isFocused) return super.onKey(event);
914 if (event.pressed) {
915 if (editor.processKey(event)) return true;
916 if (event == "S-Insert") {
917 getClipboardText(vbwin, delegate (in char[] text) {
918 if (text.length) {
919 editor.doPasteStart();
920 scope(exit) editor.doPasteEnd();
921 editor.doPutTextUtf(text[]);
922 widgetChanged();
925 return true;
927 if (event == "C-Insert") {
928 auto mtr = editor.markedBlockRange();
929 if (mtr.length) {
930 char[] brng;
931 brng.reserve(mtr.length);
932 foreach (char ch; mtr) brng ~= ch;
933 if (brng.length > 0) {
934 setClipboardText(vbwin, cast(string)brng); // it is safe to cast here
935 setPrimarySelection(vbwin, cast(string)brng); // it is safe to cast here
937 editor.doBlockResetMark();
938 widgetChanged();
940 return true;
943 if (event == "M-Tab") {
944 char[1024] abuf = void;
945 auto attname = editor.getAttachName(abuf[], editor.cury);
946 if (attname.length && editor.curx >= editor.linelen(editor.cury)) {
947 auto cplist = buildAutoCompletion(attname);
948 auto atnlen = attname.length;
949 auto adg = delegate (dynstring s) {
950 //conwriteln("attname=[", attname, "]; s=[", s, "]");
951 if (s.length <= atnlen /*|| s[0..attname.length] != attname*/) return;
952 editor.undoGroupStart();
953 scope(exit) editor.undoGroupEnd();
954 editor.doPutTextUtf(s[atnlen..$]);
955 widgetChanged();
957 if (cplist.length == 1) {
958 adg(cplist[0]);
959 } else {
960 auto acw = new SelectCompletionWindow(attname, cplist, true);
961 acw.onSelected = adg;
964 return true;
967 return super.onKey(event);
970 override bool onMouse (MouseEvent event) {
971 if (!isFocused) return super.onMouse(event);
972 if (GxPoint(event.x, event.y).inside(rect.size)) {
973 if (editor.processClick(event.x, event.y, event)) {
974 widgetChanged();
975 return true;
978 return super.onMouse(event);
981 override bool onChar (dchar ch) {
982 if (isFocused && editor.processChar(ch)) {
983 widgetChanged();
984 return true;
986 return super.onChar(ch);
991 // ////////////////////////////////////////////////////////////////////////// //
992 final class LineEditWidget : Widget {
993 TextEditor editor;
995 this (Widget aparent) {
996 tabStop = true;
997 super(aparent);
998 if (width == 0) width = 96;
999 height = gxTextHeightUtf+2;
1000 editor = new TextEditor(this, 0, 0, width, height, true); // singleline
1003 this () {
1004 assert(creatorCurrentParent !is null);
1005 this(creatorCurrentParent);
1008 @property bool readonly () const nothrow { return editor.readonly; }
1009 @property void readonly (bool v) nothrow { editor.readonly = v; }
1011 @property bool killTextOnChar () const nothrow { return editor.killTextOnChar; }
1012 @property void killTextOnChar (bool v) nothrow { editor.killTextOnChar = v; }
1014 @property dynstring str () {
1015 dynstring res;
1016 if (editor.textsize == 0) return res;
1017 res.reserve(editor.textsize);
1018 foreach (char ch; editor[]) res ~= ch;
1019 return res;
1022 @property void str (const(char)[] s) {
1023 editor.clearAndDisableUndo();
1024 scope(exit) editor.reinstantiateUndo();
1025 editor.clear();
1026 editor.doPutTextUtf(s);
1027 editor.textChanged = false;
1030 protected override void doPaint (GxRect grect) {
1031 if (width < 1 || height < 1) return;
1032 gxFillRect(grect, getColor("back"));
1033 grect.shrinkBy(0, 1);
1034 if (grect.width >= 12) grect.shrinkBy(2, 0);
1035 if (!grect.empty) {
1036 editor.moveResize(grect.x0, grect.y0, grect.width, gxTextHeightUtf);
1037 editor.drawPage();
1041 // return `true` if event was eaten
1042 override bool onKey (KeyEvent event) {
1043 if (!isFocused) return super.onKey(event);
1044 if (event.pressed) {
1045 if (editor.processKey(event)) return true;
1046 if (event == "S-Insert") {
1047 getClipboardText(vbwin, delegate (in char[] text) {
1048 if (text.length) {
1049 editor.doPasteStart();
1050 scope(exit) editor.doPasteEnd();
1051 editor.doPutTextUtf(text[]);
1052 widgetChanged();
1055 return true;
1057 if (event == "C-Insert") {
1058 auto mtr = editor.markedBlockRange();
1059 if (mtr.length) {
1060 char[] brng;
1061 brng.reserve(mtr.length);
1062 foreach (char ch; mtr) brng ~= ch;
1063 if (brng.length > 0) {
1064 setClipboardText(vbwin, cast(string)brng); // it is safe to cast here
1065 setPrimarySelection(vbwin, cast(string)brng); // it is safe to cast here
1067 editor.doBlockResetMark();
1068 widgetChanged();
1070 return true;
1073 return super.onKey(event);
1076 override bool onMouse (MouseEvent event) {
1077 if (!isFocused) return super.onMouse(event);
1078 if (GxPoint(event.x, event.y).inside(rect.size)) {
1079 if (editor.processClick(event.x, event.y, event)) {
1080 widgetChanged();
1081 return true;
1084 return super.onMouse(event);
1087 override bool onChar (dchar ch) {
1088 if (isFocused && editor.processChar(ch)) {
1089 widgetChanged();
1090 return true;
1092 return super.onChar(ch);