egra: use `memBlendColor()` in more places
[iv.d.git] / egra / gui / editor.d
blobbaa210e8178f3e5c60176a83a54fe82136fb7fba
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.strex;
23 import iv.utfutil;
24 import iv.vfs;
26 import iv.egeditor.editor;
27 //import iv.egeditor.highlighters;
29 import iv.egra.gfx;
31 import iv.egra.gui.subwindows;
32 import iv.egra.gui.widgets;
33 import iv.egra.gui.dialogs;
36 // ////////////////////////////////////////////////////////////////////////// //
37 final class ChiTextMeter : EgTextMeter {
38 GxKerning twkern;
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_";
78 int pos = 0;
79 while (pos < key.length) {
80 char ch = key[pos++];
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 ~= '_';
90 res ~= ln.stringof;
91 res ~= " () {"~code~"}";
92 return res;
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));
103 protected:
104 KeyEvent[32] comboBuf;
105 int comboCount; // number of items in `comboBuf`
106 Widget pw;
108 ChiTextMeter chiTextMeter;
110 public:
111 this (Widget apw, int x0, int y0, int w, int h, bool asinglesine=false) {
112 pw = apw;
113 //coordsInPixels = true;
114 lineHeightPixels = gxTextHeightUtf;
115 chiTextMeter = new ChiTextMeter();
116 super(x0, y0, w, h, null, asinglesine);
117 textMeter = chiTextMeter;
118 utfuck = true;
119 visualtabs = true;
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;
129 int count = 1;
130 ++pos;
131 while (pos < ts) {
132 char ch = gb[pos++];
133 if (ch == '>') ++count;
134 else if (ch == '\n') break;
135 else if (ch != ' ') break;
137 return count;
140 public override void drawCursor () {
141 if (pw.isFocused) {
142 int lcx, lcy;
143 localCursorXY(&lcx, &lcy);
144 //drawTextCursor(/*pw.parent.isFocused*/true, x0+lcx, y0+lcy);
145 pw.drawTextCursor(x0+lcx, y0+lcy, gxTextHeightUtf);
149 // not here, 'cause this is done before text
150 public override void drawStatus () {}
152 public final paintStatusLine () {
153 import core.stdc.stdio : snprintf;
154 int sx = x0, sy = y0+height;
155 gxFillRect(sx, sy, width, gxTextHeightUtf, pw.getColor("status-back"));
156 char[128] buf = void;
157 auto len = snprintf(buf.ptr, buf.length, "%04d:%04d %d", curx, cury, linecount);
158 gxDrawTextUtf(sx+2, sy, buf[0..len], pw.getColor("status-text"));
161 public override void drawPage () {
162 fullDirty(); // HACK!
163 gxWithSavedClip{
164 gxClipRect.intersect(GxRect(x0, y0, width, height));
165 super.drawPage();
167 if (!singleline) paintStatusLine();
171 public override void drawLine (int lidx, int yofs, int xskip) {
172 int x = x0-xskip;
173 int y = y0+yofs;
174 auto pos = lc.line2pos(lidx);
175 auto lea = lc.line2pos(lidx+1);
176 auto ts = gb.textsize;
177 bool utfucked = utfuck;
178 immutable int bs = bstart, be = bend;
179 immutable ls = pos;
181 uint clr = pw.getColor("text");
182 immutable uint markFgClr = pw.getColor("mark-text");
183 immutable uint markBgClr = pw.getColor("mark-back");
185 if (!singleline) {
186 int qlevel = lineQuoteLevel(lidx);
187 if (qlevel) {
188 final switch (qlevel%2) {
189 case 0: clr = pw.getColor("quote0-text"); break;
190 case 1: clr = pw.getColor("quote1-text"); break;
192 } else {
193 char[1024] abuf = void;
194 auto aname = getAttachName(abuf[], lidx);
195 if (aname.length) {
196 try {
197 import std.file : exists, isFile;
198 clr = (aname.exists && aname.isFile ? pw.getColor("attach-file-text") : pw.getColor("attach-bad-text"));
199 } catch (Exception e) {}
204 bool checkMarking = false;
205 if (hasMarkedBlock) {
206 //conwriteln("bs=", bs, "; be=", be, "; pos=", pos, "; lea=", lea);
207 if (pos >= bs && lea <= be) {
208 // full line
209 gxFillRect(x0, y, winw, lineHeightPixels, markBgClr);
210 clr = markFgClr;
211 } else if (pos < be && lea > bs) {
212 // draw block background
213 auto tpos = pos;
214 int bx0 = x, bx1 = x;
215 chiTextMeter.twkern.reset(visualtabs ? tabsize : 0);
216 while (tpos < lea) {
217 immutable dchar dch = dcharAtAdvance(tpos);
218 if (!singleline && dch == '\n') break;
219 chiTextMeter.twkern.fixWidthPre(dch);
220 if (tpos > bs) break;
222 bx0 = bx1 = x+chiTextMeter.twkern.currOfs;
223 bool eolhit = false;
224 while (tpos < lea) {
225 if (tpos >= be) break;
226 immutable dchar dch = dcharAtAdvance(tpos);
227 if (!singleline && dch == '\n') { eolhit = (tpos < be); break; }
228 chiTextMeter.twkern.fixWidthPre(dch);
229 if (tpos > be) break;
231 bx1 = (eolhit ? x0+width : x+chiTextMeter.twkern.finalWidth);
232 gxFillRect(bx0, y, bx1-bx0+1, lineHeightPixels, markBgClr);
233 checkMarking = true;
236 if (singleline && hasMarkedBlock) checkMarking = true; // let it be
238 //twkern.reset(visualtabs ? tabsize : 0);
239 uint cc = clr;
241 int epos = pos;
242 if (singleline) {
243 epos = ts;
244 } else {
245 while (epos < ts) if (gb[epos++] == '\n') { --epos; break; }
248 //FIXME: tabsize
249 int wdt = gxDrawTextUtf(GxDrawTextOptions.Tab(tabsize), x, y, this[pos..epos], delegate (in ref state) nothrow @trusted {
250 return (checkMarking ? (pos+state.spos >= bs && pos+state.epos <= be ? markFgClr : clr) : clr);
252 if (!singleline) {
253 if (epos > pos && gb[epos-1] == ' ') {
254 gxDrawChar(x+wdt, y, '\u2248', pw.getColor("wrap-mark-text"));
259 while (pos < ts) {
260 if (checkMarking) cc = (pos >= bs && pos < be ? markFgClr : clr);
261 immutable dchar dch = dcharAtAdvance(pos);
262 if (!singleline && dch == '\n') {
263 // draw "can wrap" mark
264 if (pos-2 >= ls && gb[pos-2] == ' ') {
265 int xx = x+twkern.finalWidth+1;
266 gxDrawChar(xx, y, '\n', gxRGB!(0, 0, 220));
268 break;
270 if (dch == '\t' && visualtabs) {
271 int xx = x+twkern.fixWidthPre('\t');
272 gxHLine(xx, y+lineHeightPixels/2, twkern.tablength, gxRGB!(200, 0, 0));
273 } else {
274 gxDrawChar(x+twkern.fixWidthPre(dch), y, dch, cc);
280 // just clear line
281 // use `winXXX` vars to know window dimensions
282 public override void drawEmptyLine (int yofs) {
283 // nothing to do here
286 protected enum Ecc { None, Eaten, Combo }
288 // None: not valid
289 // Eaten: exact hit
290 // Combo: combo start
291 // comboBuf should contain comboCount keys!
292 protected final Ecc checkKeys (const(char)[] keys) {
293 foreach (immutable cidx; 0..comboCount+1) {
294 keys = keys.xstrip;
295 if (keys.length == 0) return Ecc.Combo;
296 usize kepos = 0;
297 while (kepos < keys.length && keys.ptr[kepos] > ' ') ++kepos;
298 if (comboBuf[cidx] != keys[0..kepos]) return Ecc.None;
299 keys = keys[kepos..$];
301 return (keys.xstrip.length ? Ecc.Combo : Ecc.Eaten);
304 // fuck! `(this ME)` trick doesn't work here
305 protected final Ecc doEditorCommandByUDA(ME=typeof(this)) (KeyEvent key) {
306 import std.traits;
307 bool possibleCombo = false;
308 // temporarily add current key to combo
309 comboBuf[comboCount] = key;
310 // check all known combos
311 foreach (string memn; __traits(allMembers, ME)) {
312 static if (is(typeof(&__traits(getMember, ME, memn)))) {
313 import std.meta : AliasSeq;
314 alias mx = AliasSeq!(__traits(getMember, ME, memn))[0];
315 static if (isCallable!mx && hasUDA!(mx, TEDKey)) {
316 // check modifiers
317 bool goodMode = true;
318 static if (hasUDA!(mx, TEDSingleOnly)) { if (!singleline) goodMode = false; }
319 static if (hasUDA!(mx, TEDMultiOnly)) { if (singleline) goodMode = false; }
320 static if (hasUDA!(mx, TEDEditOnly)) { if (readonly) goodMode = false; }
321 static if (hasUDA!(mx, TEDROOnly)) { if (!readonly) goodMode = false; }
322 if (goodMode) {
323 foreach (const TEDKey attr; getUDAs!(mx, TEDKey)) {
324 auto cc = checkKeys(attr.key);
325 if (cc == Ecc.Eaten) {
326 // hit
327 static if (is(ReturnType!mx == void)) {
328 comboCount = 0; // reset combo
329 mx();
330 return Ecc.Eaten;
331 } else {
332 if (mx()) {
333 comboCount = 0; // reset combo
334 return Ecc.Eaten;
337 } else if (cc == Ecc.Combo) {
338 possibleCombo = true;
345 // check if we can start/continue combo
346 if (possibleCombo) {
347 if (++comboCount < comboBuf.length-1) return Ecc.Combo;
349 // if we have combo prefix, eat key unconditionally
350 if (comboCount > 0) {
351 comboCount = 0; // reset combo, too long, or invalid, or none
352 return Ecc.Eaten;
354 return Ecc.None;
357 bool processKey (KeyEvent key) {
358 final switch (doEditorCommandByUDA(key)) {
359 case Ecc.None: break;
360 case Ecc.Combo:
361 case Ecc.Eaten:
362 return true;
365 return false;
368 bool processChar (dchar ch) {
369 if (ch < ' ' || ch == 127) return false;
370 if (ch == ' ') {
371 // check if we should reformat
372 auto llen = linelen(cury);
373 if (llen >= 76) {
374 if (curx == 0 || gb[curpos-1] != ' ') doPutChar(' ');
375 auto ocx = curx;
376 auto ocy = cury;
377 // normalize ocx
379 int rpos = lc.linestart(ocy);
380 int qcnt = 0;
381 if (gb[rpos] == '>') {
382 while (gb[rpos] == '>') { ++qcnt; ++rpos; }
383 if (gb[rpos] == ' ') { ++qcnt; ++rpos; }
385 //conwriteln("origcx=", ocx, "; ncx=", ocx-qcnt, "; qcnt=", qcnt);
386 if (ocx < qcnt) ocx = qcnt; else ocx -= qcnt;
388 reformatFromLine!true(cury);
389 // position cursor
390 while (ocy < lc.linecount) {
391 int rpos = lc.linestart(ocy);
392 llen = linelen(ocy);
393 //conwriteln(" 00: ocy=", ocy, "; llen=", llen, "; ocx=", ocx);
394 int qcnt = 0;
395 if (gb[rpos] == '>') {
396 while (gb[rpos] == '>') { --llen; ++qcnt; ++rpos; }
397 if (gb[rpos] == ' ') { --llen; ++qcnt; ++rpos; }
399 //conwriteln(" 01: ocy=", ocy, "; llen=", llen, "; ocx=", ocx, "; qcnt=", qcnt);
400 if (ocx <= llen) { ocx += qcnt; break; }
401 ocx -= llen;
402 ++ocy;
404 gotoXY(ocx, ocy);
405 return true;
408 doPutDChar(ch);
409 return true;
412 bool processClick (int x, int y, MouseEvent event) {
413 if (x < 0 || y < 0 || x >= winw || y >= winh) return false;
414 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
415 int tx, ty;
416 widget2text(x, y, tx, ty);
417 gotoXY(tx, ty);
418 return true;
420 return false;
423 final:
424 void processWordWith (scope char delegate (char ch) dg) {
425 if (dg is null) return;
426 bool undoAdded = false;
427 scope(exit) if (undoAdded) undoGroupEnd();
428 auto pos = curpos;
429 if (!isWordChar(gb[pos])) return;
430 // find word start
431 while (pos > 0 && isWordChar(gb[pos-1])) --pos;
432 while (pos < gb.textsize) {
433 auto ch = gb[pos];
434 if (!isWordChar(gb[pos])) break;
435 auto nc = dg(ch);
436 if (ch != nc) {
437 if (!undoAdded) { undoAdded = true; undoGroupStart(); }
438 replaceText!"none"(pos, 1, (&nc)[0..1]);
440 ++pos;
442 gotoPos(pos);
445 final:
446 @TEDMultiOnly mixin TEDImpl!("Up", q{ doUp(); });
447 @TEDMultiOnly mixin TEDImpl!("S-Up", q{ doUp(true); });
448 @TEDMultiOnly mixin TEDImpl!("C-Up", q{ doScrollUp(); });
449 @TEDMultiOnly mixin TEDImpl!("S-C-Up", q{ doScrollUp(true); });
451 @TEDMultiOnly mixin TEDImpl!("Down", q{ doDown(); });
452 @TEDMultiOnly mixin TEDImpl!("S-Down", q{ doDown(true); });
453 @TEDMultiOnly mixin TEDImpl!("C-Down", q{ doScrollDown(); });
454 @TEDMultiOnly mixin TEDImpl!("S-C-Down", q{ doScrollDown(true); });
456 mixin TEDImpl!("Left", q{ doLeft(); });
457 mixin TEDImpl!("S-Left", q{ doLeft(true); });
458 mixin TEDImpl!("C-Left", q{ doWordLeft(); });
459 mixin TEDImpl!("S-C-Left", q{ doWordLeft(true); });
461 mixin TEDImpl!("Right", q{ doRight(); });
462 mixin TEDImpl!("S-Right", q{ doRight(true); });
463 mixin TEDImpl!("C-Right", q{ doWordRight(); });
464 mixin TEDImpl!("S-C-Right", q{ doWordRight(true); });
466 @TEDMultiOnly mixin TEDImpl!("PageUp", q{ doPageUp(); });
467 @TEDMultiOnly mixin TEDImpl!("S-PageUp", q{ doPageUp(true); });
468 @TEDMultiOnly mixin TEDImpl!("C-PageUp", q{ doTextTop(); });
469 @TEDMultiOnly mixin TEDImpl!("S-C-PageUp", q{ doTextTop(true); });
471 @TEDMultiOnly mixin TEDImpl!("PageDown", q{ doPageDown(); });
472 @TEDMultiOnly mixin TEDImpl!("S-PageDown", q{ doPageDown(true); });
473 @TEDMultiOnly mixin TEDImpl!("C-PageDown", q{ doTextBottom(); });
474 @TEDMultiOnly mixin TEDImpl!("S-C-PageDown", q{ doTextBottom(true); });
476 mixin TEDImpl!("Home", q{ doHome(); });
477 mixin TEDImpl!("S-Home", q{ doHome(true, true); });
478 @TEDMultiOnly mixin TEDImpl!("C-Home", q{ doPageTop(); });
479 @TEDMultiOnly mixin TEDImpl!("S-C-Home", q{ doPageTop(true); });
481 mixin TEDImpl!("End", q{ doEnd(); });
482 mixin TEDImpl!("S-End", q{ doEnd(true); });
483 @TEDMultiOnly mixin TEDImpl!("C-End", q{ doPageBottom(); });
484 @TEDMultiOnly mixin TEDImpl!("S-C-End", q{ doPageBottom(true); });
486 @TEDEditOnly mixin TEDImpl!("Backspace", q{ doBackspace(); });
487 @TEDSingleOnly @TEDEditOnly mixin TEDImpl!("M-Backspace", "delete previous word", q{ doDeleteWord(); });
488 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("M-Backspace", "delete previous word or unindent", q{ doBackByIndent(); });
490 mixin TEDImpl!("Delete", q{
491 doDelete();
494 //mixin TEDImpl!("^Insert", "copy block to clipboard file, reset block mark", q{ if (tempBlockFileName.length == 0) return; doBlockWrite(tempBlockFileName); doBlockResetMark(); });
496 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("Enter", q{
497 auto ly = cury;
498 if (lineQuoteLevel(ly)) {
499 doLineSplit(false); // no autoindent
500 auto ls = lc.linestart(ly);
501 undoGroupStart();
502 scope(exit) undoGroupEnd();
503 while (gb[ls] == '>') {
504 ++ls;
505 insertText!"end"(curpos, ">");
507 if (gb[curpos] > ' ') insertText!"end"(curpos, " ");
508 } else {
509 doLineSplit();
512 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("M-Enter", "split line without autoindenting", q{ doLineSplit(false); });
515 mixin TEDImpl!("F3", "start/stop/reset block marking", q{ doToggleBlockMarkMode(); });
516 mixin TEDImpl!("C-F3", "reset block mark", q{ doBlockResetMark(); });
517 @TEDEditOnly mixin TEDImpl!("F5", "copy block", q{ doBlockCopy(); });
518 // mixin TEDImpl!("^F5", "copy block to clipboard file", q{ if (tempBlockFileName.length == 0) return; doBlockWrite(tempBlockFileName); });
519 //@TEDEditOnly mixin TEDImpl!("S-F5", "insert block from clipboard file", q{ if (tempBlockFileName.length == 0) return; waitingInF5 = true; });
520 @TEDEditOnly mixin TEDImpl!("F6", "move block", q{ doBlockMove(); });
521 @TEDEditOnly mixin TEDImpl!("F8", "delete block", q{ doBlockDelete(); });
523 mixin TEDImpl!("C-A", "move to line start", q{ doHome(); });
524 mixin TEDImpl!("C-E", "move to line end", q{ doEnd(); });
526 @TEDMultiOnly mixin TEDImpl!("M-I", "jump to previous bookmark", q{ doBookmarkJumpUp(); });
527 @TEDMultiOnly mixin TEDImpl!("M-J", "jump to next bookmark", q{ doBookmarkJumpDown(); });
528 @TEDMultiOnly mixin TEDImpl!("M-K", "toggle bookmark", q{ doBookmarkToggle(); });
530 @TEDEditOnly mixin TEDImpl!("M-C", "capitalize word", q{
531 bool first = true;
532 processWordWith((char ch) {
533 if (first) { first = false; ch = ch.toupper; }
534 return ch;
537 @TEDEditOnly mixin TEDImpl!("M-Q", "lowercase word", q{ processWordWith((char ch) => ch.tolower); });
538 @TEDEditOnly mixin TEDImpl!("M-U", "uppercase word", q{ processWordWith((char ch) => ch.toupper); });
540 @TEDMultiOnly mixin TEDImpl!("M-S-L", "force center current line", q{ makeCurLineVisibleCentered(true); });
541 @TEDEditOnly mixin TEDImpl!("C-U", "undo", q{ doUndo(); });
542 @TEDEditOnly mixin TEDImpl!("M-S-U", "redo", q{ doRedo(); });
543 @TEDEditOnly mixin TEDImpl!("C-W", "remove previous word", q{ doDeleteWord(); });
544 @TEDEditOnly mixin TEDImpl!("C-Y", "remove current line", q{ doKillLine(); });
546 //@TEDMultiOnly @TEDEditOnly mixin TEDImpl!("Tab", q{ doPutText(" "); });
547 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-Tab", "indent block", q{ doIndentBlock(); });
548 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-S-Tab", "unindent block", q{ doUnindentBlock(); });
550 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-I", "indent block", q{ doIndentBlock(); });
551 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-U", "unindent block", q{ doUnindentBlock(); });
552 @TEDEditOnly mixin TEDImpl!("C-K C-E", "clear from cursor to EOL", q{ doKillToEOL(); });
553 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K Tab", "indent block", q{ doIndentBlock(); });
554 // @TEDEditOnly mixin TEDImpl!("^K M-Tab", "untabify", q{ doUntabify(gb.tabsize ? gb.tabsize : 2); }); // alt+tab: untabify
555 // @TEDEditOnly mixin TEDImpl!("^K C-space", "remove trailing spaces", q{ doRemoveTailingSpaces(); });
556 // mixin TEDImpl!("C-K C-T", /*"toggle \"visual tabs\" mode",*/ q{ visualtabs = !visualtabs; });
558 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-B", q{ doSetBlockStart(); });
559 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-K", q{ doSetBlockEnd(); });
561 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-C", q{ doBlockCopy(); });
562 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-M", q{ doBlockMove(); });
563 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-Y", q{ doBlockDelete(); });
564 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-H", q{ doBlockResetMark(); });
565 // fuckin' vt100!
566 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K Backspace", q{ doBlockResetMark(); });
568 @TEDEditOnly mixin TEDImpl!("C-Q Tab", q{ doPutChar('\t'); });
569 //mixin TEDImpl!("C-Q C-U", "toggle utfuck mode", q{ utfuck = !utfuck; }); // ^Q^U: switch utfuck mode
570 //mixin TEDImpl!("C-Q 1", "switch to koi8", q{ utfuck = false; codepage = CodePage.koi8u; fullDirty(); });
571 //mixin TEDImpl!("C-Q 2", "switch to cp1251", q{ utfuck = false; codepage = CodePage.cp1251; fullDirty(); });
572 //mixin TEDImpl!("C-Q 3", "switch to cp866", q{ utfuck = false; codepage = CodePage.cp866; fullDirty(); });
573 mixin TEDImpl!("C-Q C-B", "go to block start", q{ if (hasMarkedBlock) gotoPos!true(bstart); lastBGEnd = false; });
575 @TEDSingleOnly @TEDEditOnly mixin TEDImpl!("C-Q Enter", "intert LF", q{ doPutChar('\n'); });
576 @TEDSingleOnly @TEDEditOnly mixin TEDImpl!("C-Q M-Enter", "intert CR", q{ doPutChar('\r'); });
578 mixin TEDImpl!("C-Q C-K", "go to block end", q{ if (hasMarkedBlock) gotoPos!true(bend); lastBGEnd = true; });
580 @TEDMultiOnly @TEDROOnly mixin TEDImpl!("Space", q{ doPageDown(); });
581 @TEDMultiOnly @TEDROOnly mixin TEDImpl!("S-Space", q{ doPageUp(); });
583 final:
584 string[] extractAttaches () {
585 string[] res;
586 int lidx = 0;
587 lineloop: while (lidx < lc.linecount) {
588 // find "@@"
589 auto pos = lc.linestart(lidx);
590 //conwriteln("checking line ", lidx, ": '", gb[pos], "'");
591 while (pos < gb.textsize) {
592 char ch = gb[pos];
593 if (ch == '@' && gb[pos+1] == '@') { pos += 2; break; } // found
594 if (ch > ' ' || ch == '\n') { ++lidx; continue lineloop; } // next line
595 ++pos;
597 //conwriteln("found \"@@\" at line ", lidx);
598 // skip spaces after "@@"
599 while (pos < gb.textsize) {
600 char ch = gb[pos];
601 if (ch == '\n') { ++lidx; continue lineloop; } // next line
602 if (ch > ' ') break;
603 ++pos;
605 if (pos >= gb.textsize) break; // no more text
606 // extract file name
607 string fname;
608 while (pos < gb.textsize) {
609 char ch = gb[pos];
610 if (ch == '\n') break;
611 fname ~= ch;
612 ++pos;
614 if (fname.length) {
615 //conwriteln("got fname: '", fname, "'");
616 res ~= fname;
618 // remove this line
619 int ls = lc.linestart(lidx);
620 int le = lc.linestart(lidx+1);
621 if (ls < le) deleteText!"start"(ls, le-ls);
623 return res;
626 char[] getAttachName (char[] dest, int lidx) {
627 if (lidx < 0 || lidx >= lc.linecount) return null;
628 // find "@@"
629 auto pos = lc.linestart(lidx);
630 while (pos < gb.textsize) {
631 char ch = gb[pos];
632 if (ch == '@' && gb[pos+1] == '@') { pos += 2; break; } // found
633 if (ch > ' ' || ch == '\n') return null; // not found
634 ++pos;
636 // skip spaces after "@@"
637 while (pos < gb.textsize) {
638 char ch = gb[pos];
639 if (ch == '\n') return null; // not found
640 if (ch > ' ') break;
641 ++pos;
643 if (pos >= gb.textsize) return null; // not found
644 // extract file name
645 usize dpos = 0;
646 while (pos < gb.textsize) {
647 char ch = gb[pos];
648 if (ch == '\n') break;
649 if (dpos >= dest.length) return null;
650 dest.ptr[dpos++] = ch;
651 ++pos;
653 return dest.ptr[0..dpos];
656 // ah, who cares about speed?
657 void reformatFromLine(bool doUndoGroup) (int lidx) {
658 if (lidx < 0) lidx = 0;
659 if (lidx >= lc.linecount) return;
661 static if (doUndoGroup) {
662 bool undoGroupStarted = false;
663 scope(exit) if (undoGroupStarted) undoGroupEnd();
665 void startUndoGroup () {
666 if (!undoGroupStarted) {
667 undoGroupStarted = true;
668 undoGroupStart();
671 } else {
672 void startUndoGroup () {}
675 void normalizeQuoting (int lidx) {
676 while (lidx < lc.linecount) {
677 auto pos = lc.linestart(lidx);
678 if (gb[pos] == '>') {
679 bool lastWasSpace = false;
680 auto stpos = pos;
681 auto afterlastq = pos;
682 while (pos < gb.textsize) {
683 char ch = gb[pos];
684 if (ch == '\n') break;
685 if (ch == '>') { afterlastq = ++pos; continue; }
686 if (ch == ' ') { ++pos; continue; }
687 break;
689 pos = stpos;
690 while (pos < afterlastq) {
691 char ch = gb[pos];
692 assert(ch != '\n');
693 if (ch == ' ') { deleteText(pos, 1); --afterlastq; continue; } // remove space (thus normalizing quotes)
694 assert(ch == '>');
695 ++pos;
697 assert(pos == afterlastq);
698 if (pos < gb.textsize && gb[pos] > ' ') { startUndoGroup(); insertText!("none", false)(pos, " "); }
700 ++lidx;
704 // try to join two lines, if it is possible; return `true` if succeed
705 bool tryJoinLines (int lidx) {
706 assert(lidx >= 0);
707 if (lc.linecount == 1) return false; // nothing to do
708 if (lidx+1 >= lc.linecount) return false; // nothing to do
709 auto ql0 = lineQuoteLevel(lidx);
710 auto ql1 = lineQuoteLevel(lidx+1);
711 if (ql0 != ql1) return false; // different quote levels, can't join
712 auto ls = lc.linestart(lidx);
713 auto le = lc.lineend(lidx);
714 if (le-ls < 1) return false; // wtf?!
715 if (gb[le-1] != ' ') return false; // no trailing space -- can't join
716 // don't join if next line is empty one: this is prolly paragraph delimiter
717 if (le+1 >= gb.textsize || gb[le+1] == '\n') return false;
718 if (gb[le+1] == '>') {
719 int pp = le+1;
720 while (pp < gb.textsize) {
721 char ch = gb[pp];
722 if (ch != '>' && ch != ' ') break;
723 ++pp;
725 if (gb[pp-1] != ' ') return false;
727 // remove newline
728 startUndoGroup();
729 deleteText(le, 1);
730 // remove excessive spaces, if any
731 while (le > ls && gb[le-1] == ' ') --le;
732 assert(gb[le] == ' ');
733 ++le; // but leave one space
734 while (le < gb.textsize) {
735 if (gb[le] == '\n' || gb[le] > ' ') break;
736 deleteText(le, 1);
738 // remove quoting
739 if (ql0) {
740 assert(le < gb.textsize && gb[le] == '>');
741 while (le < gb.textsize) {
742 char ch = gb[le];
743 if (ch == '\n' || (ch != '>' && ch != ' ')) break;
744 deleteText(le, 1);
747 return true; // yay
750 // join lines; we'll split 'em later
751 for (int l = lidx; l < lc.linecount; ) if (!tryJoinLines(l)) ++l;
753 // make quoting consistent
754 normalizeQuoting(lidx);
756 void conwrlinerest (int pos) {
757 if (pos < 0 || pos >= gb.textsize) return;
758 while (pos < gb.textsize) {
759 char ch = gb[pos++];
760 if (ch == '\n') break;
761 conwrite(ch);
765 void conwrline (int lidx) {
766 if (lidx < 0 || lidx >= lc.linecount) return;
767 conwrlinerest(lc.linestart(lidx));
770 // now split the lines; all lines are joined, so we can only split
771 lineloop: while (lidx < lc.linecount) {
772 auto ls = lc.linestart(lidx);
773 auto le = lc.lineend(lidx);
774 // calculate line length without trailing spaces
775 auto llen = linelen(lidx);
777 auto pe = le;
778 while (pe > ls && gb[pe-1] <= ' ') { --pe; --llen; }
780 if (llen <= 76) { ++lidx; continue; } // nothing to do here
781 // need to wrap it
782 auto pos = ls;
783 int curlen = 0;
784 // skip quotes, if any
785 while (gb[pos] == '>') { ++curlen; ++pos; }
786 // skip leading spaces
787 while (gb[pos] != '\n' && gb[pos] <= ' ') { ++curlen; ++pos; }
788 if (pos >= gb.textsize || gb[pos] == '\n') { ++lidx; continue; } // wtf?!
789 int lwstart = -1;
790 bool inword = true;
791 while (pos < gb.textsize) {
792 immutable stpos = pos;
793 dchar ch = dcharAtAdvance(pos);
794 ++curlen;
795 if (inword) {
796 if (ch <= ' ') {
797 if (lwstart >= 0 && curlen > 76) {
798 // wrap
799 startUndoGroup();
800 insertText!("none", false)(lwstart, "\n");
801 ++lwstart;
802 if (gb[ls] == '>') {
803 while (gb[ls] == '>') {
804 insertText!("none", false)(lwstart, ">");
805 ++lwstart;
806 ++ls;
808 insertText!("none", false)(lwstart, " ");
810 ++lidx;
811 continue lineloop;
813 if (ch == '\n') break;
814 // not in word anymore
815 inword = false;
817 } else {
818 if (ch == '\n') break;
819 if (ch > ' ') { lwstart = stpos; inword = true; }
822 ++lidx;
828 // ////////////////////////////////////////////////////////////////////////// //
829 struct QuoteInfo {
830 int level; // quote level
831 int length; // quote prefix length, in chars
834 QuoteInfo calcQuote(T:const(char)[]) (T s) {
835 static if (is(T == typeof(null))) {
836 return QuoteInfo();
837 } else {
838 QuoteInfo qi;
839 if (s.length > 0 && s[0] == '>') {
840 while (qi.length < s.length) {
841 if (s[qi.length] != ' ') {
842 if (s[qi.length] != '>') break;
843 ++qi.level;
845 ++qi.length;
847 if (s.length-qi.length > 1 && s[qi.length] == ' ') ++qi.length;
849 return qi;
854 // ////////////////////////////////////////////////////////////////////////// //
855 final class EditorWidget : Widget {
856 TextEditor editor;
857 bool moveToBottom = true;
859 this (Widget aparent) {
860 tabStop = true;
861 super(aparent);
862 editor = new TextEditor(this, 0, 0, 10, 10);
865 this () {
866 assert(creatorCurrentParent !is null);
867 this(creatorCurrentParent);
870 void addText (const(char)[] s) {
871 immutable GxRect grect = globalRect;
872 editor.moveResize(grect.x0, grect.y0, (grect.width < 1 ? 1 : grect.width), (grect.height < 1 ? grect.height : 1));
873 editor.doPasteStart();
874 scope(exit) editor.doPasteEnd();
875 editor.doPutTextUtf(s);
876 //editor.doPutChar('\n');
879 void reformat () {
880 editor.clearAndDisableUndo();
881 scope(exit) editor.reinstantiateUndo();
882 editor.reformatFromLine!false(0);
883 editor.gotoXY(0, 0); // HACK
884 editor.textChanged = false;
887 string[] extractAttaches () {
888 return editor.extractAttaches();
891 private final void drawScrollBar () {
892 //restoreClip(); // the easiest way again
893 immutable GxRect grect = globalRect;
894 gxDrawScrollBar(GxRect(grect.x0, grect.y0, 4, height), editor.linecount-1, editor.topline+editor.visibleLinesPerWindow-1);
897 protected override void doPaint (GxRect grect) {
898 if (width < 1 || height < 1) return;
899 editor.moveResize(grect.x0+5, grect.y0, grect.width-5*2, grect.height-gxTextHeightUtf);
901 if (moveToBottom) { moveToBottom = false; editor.gotoXY(0, editor.linecount); } // HACK!
902 gxFillRect(grect, getColor("back"));
903 gxWithSavedClip {
904 editor.drawPage();
907 drawScrollBar();
910 // return `true` if event was eaten
911 override bool onKey (KeyEvent event) {
912 if (!isFocused) return super.onKey(event);
913 postScreenRebuild();
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[]);
924 return true;
926 if (event == "C-Insert") {
927 auto mtr = editor.markedBlockRange();
928 if (mtr.length) {
929 char[] brng;
930 brng.reserve(mtr.length);
931 foreach (char ch; mtr) brng ~= ch;
932 if (brng.length > 0) {
933 setClipboardText(vbwin, cast(string)brng); // it is safe to cast here
934 setPrimarySelection(vbwin, cast(string)brng); // it is safe to cast here
936 editor.doBlockResetMark();
938 return true;
941 if (event == "M-Tab") {
942 char[1024] abuf = void;
943 auto attname = editor.getAttachName(abuf[], editor.cury);
944 if (attname.length && editor.curx >= editor.linelen(editor.cury)) {
945 auto cplist = buildAutoCompletion(attname);
946 auto atnlen = attname.length;
947 auto adg = delegate (string s) {
948 //conwriteln("attname=[", attname, "]; s=[", s, "]");
949 if (s.length <= atnlen /*|| s[0..attname.length] != attname*/) return;
950 editor.undoGroupStart();
951 scope(exit) editor.undoGroupEnd();
952 editor.doPutTextUtf(s[atnlen..$]);
954 if (cplist.length == 1) {
955 adg(cplist[0]);
956 } else {
957 auto acw = new SelectCompletionWindow(attname, cplist, true);
958 acw.onSelected = adg;
961 return true;
964 return super.onKey(event);
967 override bool onMouse (MouseEvent event) {
968 if (!isFocused) return super.onMouse(event);
969 if (GxPoint(event.x, event.y).inside(rect.size)) {
970 if (editor.processClick(event.x, event.y, event)) {
971 postScreenRebuild();
972 return true;
975 return super.onMouse(event);
978 override bool onChar (dchar ch) {
979 if (isFocused && editor.processChar(ch)) {
980 postScreenRebuild();
981 return true;
983 return super.onChar(ch);
988 // ////////////////////////////////////////////////////////////////////////// //
989 final class LineEditWidget : Widget {
990 TextEditor editor;
992 this (Widget aparent) {
993 tabStop = true;
994 super(aparent);
995 if (width == 0) width = 96;
996 height = gxTextHeightUtf+2;
997 editor = new TextEditor(this, 0, 0, width, height, true); // singleline
1000 this () {
1001 assert(creatorCurrentParent !is null);
1002 this(creatorCurrentParent);
1005 @property bool readonly () const nothrow { return editor.readonly; }
1006 @property void readonly (bool v) nothrow { editor.readonly = v; }
1008 @property bool killTextOnChar () const nothrow { return editor.killTextOnChar; }
1009 @property void killTextOnChar (bool v) nothrow { editor.killTextOnChar = v; }
1011 @property string str () {
1012 if (editor.textsize == 0) return null;
1013 char[] res;
1014 res.reserve(editor.textsize);
1015 foreach (char ch; editor[]) res ~= ch;
1016 return cast(string)res; // it is safe to cast here
1019 @property void str (const(char)[] s) {
1020 editor.clearAndDisableUndo();
1021 scope(exit) editor.reinstantiateUndo();
1022 editor.clear();
1023 editor.doPutTextUtf(s);
1024 editor.textChanged = false;
1027 protected override void doPaint (GxRect grect) {
1028 if (width < 1 || height < 1) return;
1029 gxFillRect(grect, getColor("back"));
1030 grect.shrinkBy(0, 1);
1031 if (grect.width >= 12) grect.shrinkBy(2, 0);
1032 if (!grect.empty) {
1033 editor.moveResize(grect.x0, grect.y0, grect.width, gxTextHeightUtf);
1034 editor.drawPage();
1038 // return `true` if event was eaten
1039 override bool onKey (KeyEvent event) {
1040 if (!isFocused) return super.onKey(event);
1041 postScreenRebuild();
1042 if (event.pressed) {
1043 if (editor.processKey(event)) return true;
1044 if (event == "S-Insert") {
1045 getClipboardText(vbwin, delegate (in char[] text) {
1046 if (text.length) {
1047 editor.doPasteStart();
1048 scope(exit) editor.doPasteEnd();
1049 editor.doPutTextUtf(text[]);
1052 return true;
1054 if (event == "C-Insert") {
1055 auto mtr = editor.markedBlockRange();
1056 if (mtr.length) {
1057 char[] brng;
1058 brng.reserve(mtr.length);
1059 foreach (char ch; mtr) brng ~= ch;
1060 if (brng.length > 0) {
1061 setClipboardText(vbwin, cast(string)brng); // it is safe to cast here
1062 setPrimarySelection(vbwin, cast(string)brng); // it is safe to cast here
1064 editor.doBlockResetMark();
1066 return true;
1069 return super.onKey(event);
1072 override bool onMouse (MouseEvent event) {
1073 if (!isFocused) return super.onMouse(event);
1074 if (GxPoint(event.x, event.y).inside(rect.size)) {
1075 if (editor.processClick(event.x, event.y, event)) {
1076 postScreenRebuild();
1077 return true;
1080 return super.onMouse(event);
1083 override bool onChar (dchar ch) {
1084 if (isFocused && editor.processChar(ch)) {
1085 postScreenRebuild();
1086 return true;
1088 return super.onChar(ch);