sq3: show SQLite error messages on stderr by default
[iv.d.git] / egtui / editor / ttyeditor.d
blob7e3b8a8c1908fbb0750a6aaab154b2740fb058a2
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.egtui.editor.ttyeditor /*is aliced*/;
18 import std.datetime : SysTime;
20 import iv.alice;
21 import iv.rawtty;
22 import iv.srex;
23 import iv.strex;
24 import iv.utfutil;
25 import iv.vfs.io;
27 import iv.egtui.tty;
28 import iv.egtui.tui;
29 import iv.egtui.dialogs;
30 import iv.egeditor.editor;
31 import iv.egeditor.highlighters;
32 import iv.egtui.editor.highlighters;
33 import iv.egtui.editor.dialogs;
36 // ////////////////////////////////////////////////////////////////////////// //
37 private string normalizedAbsolutePath (string path) {
38 import std.path;
39 return buildNormalizedPath(absolutePath(path));
43 // ////////////////////////////////////////////////////////////////////////// //
44 class TtyEditor : EditorEngine {
45 enum TEDSingleOnly; // only for single-line mode
46 enum TEDMultiOnly; // only for multiline mode
47 enum TEDEditOnly; // only for non-readonly mode
48 enum TEDROOnly; // only for readonly mode
49 enum TEDRepeated; // allow "repeation count"
50 enum TEDRepeatChar; // this combo meant "repeat mode"
52 static struct TEDKey { string key; string help; bool hidden; } // UDA
54 static string TEDImplX(string key, string help, string code, usize ln) () {
55 static assert(key.length > 0, "wtf?!");
56 static assert(code.length > 0, "wtf?!");
57 string res = "@TEDKey("~key.stringof~", "~help.stringof~") void _ted_";
58 int pos = 0;
59 while (pos < key.length) {
60 char ch = key[pos++];
61 if (key.length-pos > 0 && key[pos] == '-') {
62 if (ch == 'C' || ch == 'c') { ++pos; res ~= "Ctrl"; continue; }
63 if (ch == 'M' || ch == 'm') { ++pos; res ~= "Alt"; continue; }
64 if (ch == 'S' || ch == 's') { ++pos; res ~= "Shift"; continue; }
66 if (ch == '^') { res ~= "Ctrl"; continue; }
67 if (ch >= 'a' && ch <= 'z') ch -= 32;
68 if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || ch == '_') res ~= ch; else res ~= '_';
70 res ~= ln.stringof;
71 res ~= " () {"~code~"}";
72 return res;
75 mixin template TEDImpl(string key, string help, string code, usize ln=__LINE__) {
76 mixin(TEDImplX!(key, help, code, ln));
79 mixin template TEDImpl(string key, string code, usize ln=__LINE__) {
80 mixin(TEDImplX!(key, "", code, ln));
83 protected:
84 TtyEvent[32] comboBuf;
85 int comboCount; // number of items in `comboBuf`
86 bool waitingInF5;
87 bool incInputActive;
89 enum RepMode {
90 Normal,
91 JustStarted,
92 CSpace, // C-Space just pressed
93 // math ops; they pushing value to stack
94 Plus,
95 Minus,
96 Mul,
97 Div,
100 int repeatCounter = -1; // <0: not in counter typing
101 int repeatIteration; // 0...
102 RepMode repeatMode; // <0: C-Space just pressed; >0: just started; 0: normal
103 int[8] repeatStack;
104 int repeatSP;
106 protected:
107 TtyEditor mPromptInput; // input line for a prompt; lazy creation
108 bool mPromptActive;
109 char[128] mPromptPrompt; // lol
110 int mPromptLen;
112 final void promptDeactivate () {
113 if (mPromptActive) {
114 mPromptActive = false;
115 fullDirty(); // just in case
119 final void promptNoKillText () {
120 if (mPromptInput !is null) mPromptInput.killTextOnChar = false;
123 final void promptActivate (const(char)[] prompt=null, const(char)[] text=null) {
124 if (mPromptInput is null) {
125 mPromptInput = new TtyEditor(0, 0, 10, 1, true); // single-lined
128 bool addDotDotDot = false;
129 if (winw <= 8) {
130 prompt = null;
131 } else {
132 if (prompt.length > winw-8) {
133 addDotDotDot = true;
134 prompt = prompt[$-(winw-8)..$];
137 if (prompt.length > mPromptPrompt.length) addDotDotDot = true;
139 mPromptPrompt[] = 0;
140 if (addDotDotDot) {
141 mPromptPrompt[0..3] = '.';
142 if (prompt.length > mPromptPrompt.length-3) prompt = prompt[$-(mPromptPrompt.length-3)..$];
143 mPromptPrompt[3..3+prompt.length] = prompt[];
144 mPromptLen = cast(int)prompt.length+3;
145 } else {
146 mPromptPrompt[0..prompt.length] = prompt[];
147 mPromptLen = cast(int)prompt.length;
150 mPromptInput.moveResize(winx+mPromptLen+2, winy-1, winw-mPromptLen-2, 1);
151 mPromptInput.clear();
152 if (text.length) {
153 mPromptInput.doPutText(text);
154 mPromptInput.clearUndo();
157 mPromptInput.killTextOnChar = true;
158 mPromptInput.clrBlock = XtColorFB!(TtyRgb2Color!(0xff, 0xff, 0xff), TtyRgb2Color!(0x00, 0x5f, 0xff));
159 mPromptInput.clrText = XtColorFB!(TtyRgb2Color!(0x00, 0x00, 0x00), TtyRgb2Color!(0xff, 0x7f, 0x00));
160 mPromptInput.clrTextUnchanged = XtColorFB!(TtyRgb2Color!(0x00, 0x00, 0x00), TtyRgb2Color!(0xcf, 0x4f, 0x00));
162 mPromptInput.utfuck = utfuck;
163 mPromptInput.codepage = codepage;
164 mPromptActive = true;
165 fullDirty(); // just in case
168 final bool promptProcessKey (TtyEvent key, scope void delegate (EditorEngine ed) onChange=null) {
169 if (!mPromptActive) return false;
170 auto lastCC = mPromptInput.bufferCC;
171 auto res = mPromptInput.processKey(key);
172 if (lastCC != mPromptInput.bufferCC && onChange !is null) onChange(mPromptInput);
173 return res;
176 protected:
177 int incSearchDir; // -1; 0; 1
178 char[] incSearchBuf; // will be actively reused, don't expose
179 int incSearchHitPos = -1;
180 int incSearchHitLen = -1;
182 protected:
183 // autocompletion buffers
184 const(char)[][256] aclist; // autocompletion tokens
185 uint acused = 0; // number of found tokens
186 char[] acbuffer; // will be actively reused, don't expose
188 public:
189 string logFileName;
190 string tempBlockFileName;
191 string fullFileName;
192 // save this, so we can check on saving
193 SysTime fileModTime;
194 long fileDiskSize = -1; // <0: modtime is invalid
195 bool dontSetCursor; // true: don't gotoxy to cursor position
196 // colors; valid for singleline control
197 uint clrBlock, clrText, clrTextUnchanged;
198 // other
199 SearchReplaceOptions srrOptions;
200 bool hideStatus = false;
201 bool hideSBar = false; // hide scrollbar
202 FuiHistoryManager hisman; // history manager for dialogs
204 public:
205 this (int x0, int y0, int w, int h, bool asinglesine=false) {
206 // prompt should be created only for multiline editors
207 super(x0, y0, w, h, null, asinglesine);
208 srrOptions.type = SearchReplaceOptions.Type.Regex;
209 srrOptions.casesens = true;
210 srrOptions.nocomments = true;
213 // call this after setting `fullFileName`
214 void setupHighlighter () {
215 auto ep = fullFileName.length;
216 while (ep > 0 && fullFileName.ptr[ep-1] != '/' && fullFileName.ptr[ep-1] != '.') --ep;
217 if (ep < 1 || fullFileName.ptr[ep-1] != '.') {
218 attachHiglighter(getHiglighterObjectFor("", fullFileName));
219 } else {
220 attachHiglighter(getHiglighterObjectFor(fullFileName[ep-1..$], fullFileName));
224 final void toggleHighlighting () {
225 if (hl is null) setupHighlighter(); else detachHighlighter();
228 final @property bool hasHighlighter () const pure nothrow @safe @nogc { pragma(inline, true); return (hl !is null); }
230 final void getDiskFileInfo () {
231 import std.file : getSize, timeLastModified;
232 if (fullFileName.length) {
233 fileDiskSize = getSize(fullFileName);
234 fileModTime = timeLastModified(fullFileName);
235 } else {
236 fileDiskSize = -1;
240 // return `true` if file was changed
241 final bool wasDiskFileChanged () {
242 import std.file : exists, getSize, timeLastModified;
243 if (fullFileName.length && fileDiskSize >= 0) {
244 if (!fullFileName.exists) return false;
245 auto sz = getSize(fullFileName);
246 if (sz != fileDiskSize) return true;
247 SysTime modtime = timeLastModified(fullFileName);
248 if (modtime != fileModTime) return true;
250 return false;
253 override void loadFile (const(char)[] fname) {
254 fileDiskSize = -1;
255 fullFileName = normalizedAbsolutePath(fname.idup);
256 super.loadFile(VFile(fullFileName));
257 getDiskFileInfo();
260 // return `false` iff overwrite dialog was aborted
261 final bool saveFileChecked () {
262 if (fullFileName.length == 0) return true;
263 if (wasDiskFileChanged) {
264 auto res = dialogFileModified(fullFileName, false, "Disk file was changed. Overwrite it?");
265 if (res < 0) return false;
266 if (res == 1) {
267 super.saveFile(fullFileName);
268 getDiskFileInfo();
270 } else {
271 super.saveFile(fullFileName);
272 getDiskFileInfo();
274 return true;
277 final void checkDiskAndReloadPrompt () {
278 if (fullFileName.length == 0) return;
279 if (wasDiskFileChanged) {
280 auto fn = fullFileName;
281 if (fn.length > ttyw-3) fn = "* ..."~fn[$-(ttyw-4)..$];
282 auto res = dialogFileModified(fullFileName, false, "Disk file was changed. Reload it?");
283 if (res != 1) return;
284 int rx = cx, ry = cy;
285 clear();
286 super.loadFile(VFile(fullFileName));
287 getDiskFileInfo();
288 cx = rx;
289 cy = ry;
290 normXY();
291 makeCurLineVisibleCentered();
295 override void saveFile (const(char)[] fname=null) {
296 if (fname.length) {
297 auto nfn = normalizedAbsolutePath(fname.idup);
298 if (fname == fullFileName) { saveFileChecked(); return; }
299 super.saveFile(VFile(nfn, "w"));
300 fullFileName = nfn;
301 getDiskFileInfo();
302 } else {
303 saveFileChecked();
307 protected override void willBeDeleted (int pos, int len, int eolcount) {
308 if (len > 0) resetIncSearchPos();
309 super.willBeDeleted(pos, len, eolcount);
312 protected override void willBeInserted (int pos, int len, int eolcount) {
313 if (len > 0) resetIncSearchPos();
314 super.willBeInserted(pos, len, eolcount);
317 final void resetIncSearchPos () nothrow {
318 if (incSearchHitPos >= 0) {
319 markLinesDirty(lc.pos2line(incSearchHitPos), 1);
320 incSearchHitPos = -1;
321 incSearchHitLen = -1;
325 final void doStartIncSearch (int sdir=0) {
326 resetIncSearchPos();
327 incInputActive = true;
328 incSearchDir = (sdir ? sdir : incSearchDir ? incSearchDir : 1);
329 promptActivate("incsearch", incSearchBuf);
331 incSearchBuf.length = 0;
332 incSearchBuf.assumeSafeAppend;
336 final void doNextIncSearch (bool domove=true) {
337 if (incSearchDir == 0 || incSearchBuf.length == 0) {
338 resetIncSearchPos();
339 return;
341 //gb.moveGapAtEnd();
342 //TODO: use `memr?chr()` here?
343 resetIncSearchPos();
344 if (incSearchBuf.ptr[0] != '/' || incSearchBuf.ptr[0] == '\\') {
345 // plain text
346 int isbofs = 0;
347 if (incSearchBuf.ptr[0] == '\\') {
348 if (incSearchBuf.length == 1) return;
349 isbofs = 1;
351 int pos = curpos+(domove ? incSearchDir : 0);
352 PlainMatch mt;
353 if (incSearchDir < 0) {
354 mt = findTextPlainBack(incSearchBuf[isbofs..$], 0, pos, /*words:*/false, /*caseSens:*/true);
355 } else {
356 mt = findTextPlain(incSearchBuf[isbofs..$], pos, textsize, /*words:*/false, /*caseSens:*/true);
358 if (!mt.empty) {
359 incSearchHitPos = mt.s;
360 incSearchHitLen = mt.e-mt.s;
362 } else if (incSearchBuf.length > 2 && incSearchBuf[$-1] == '/') {
363 // regexp
364 import std.utf : byChar;
365 auto re = RegExp.create(incSearchBuf[1..$-1].byChar, SRFlags.Multiline);
366 if (!re.valid) { ttyBeep; return; }
367 Pike.Capture[2] caps;
368 bool found;
369 if (incSearchDir > 0) {
370 found = findTextRegExp(re, curpos+(domove ? 1 : 0), textsize, caps);
371 } else {
372 found = findTextRegExpBack(re, 0, curpos+(domove ? -1 : 0), caps);
374 if (found) {
375 // something was found
376 incSearchHitPos = caps[0].s;
377 incSearchHitLen = caps[0].e-caps[0].s;
380 if (incSearchHitPos >= 0 && incSearchHitLen > 0) {
381 static if (EgEditorMovementUndo) pushUndoCurPos();
382 lc.pos2xy(incSearchHitPos, cx, cy);
383 makeCurLineVisibleCentered();
384 markRangeDirty(incSearchHitPos, incSearchHitLen);
388 final void drawScrollBar () {
389 if (winx == 0) return; // it won't be visible anyway
390 if (singleline || hideSBar) return;
391 auto win = XtWindow(winx-1, winy-(hideStatus ? 0 : 1), 1, winh+(hideStatus ? 0 : 1));
392 if (win.height < 1) return; // it won't be visible anyway
393 win.fg = TtyRgb2Color!(0x00, 0x00, 0x00);
394 win.bg = (termType != TermType.linux ? TtyRgb2Color!(0x00, 0x5f, 0xaf) : TtyRgb2Color!(0x00, 0x5f, 0xcf));
395 int filled;
396 int botline = topline+winh-1;
397 if (botline >= linecount-1 || linecount == 0) {
398 filled = win.height;
399 } else {
400 filled = (win.height-1)*botline/linecount;
401 if (filled == win.height-1 && mTopLine+winh < linecount) --filled;
403 foreach (immutable y; 0..win.height) win.writeCharsAt!true(0, y, 1, (y <= filled ? ' ' : 'a'));
406 public override void drawCursor () {
407 if (dontSetCursor) return;
408 // draw prompt if it is active
409 if (mPromptActive) { mPromptInput.drawCursor(); return; }
410 int rx, ry;
411 lc.pos2xyVT(curpos, rx, ry);
412 XtWindow(winx, winy, winw, winh).gotoXY(rx-mXOfs, ry-topline);
415 public override void drawStatus () {
416 if (singleline || hideStatus) return;
417 auto win = XtWindow(winx, winy-1, winw, 1);
418 win.fg = TtyRgb2Color!(0x00, 0x00, 0x00);
419 win.bg = TtyRgb2Color!(0xb2, 0xb2, 0xb2);
420 win.writeCharsAt(0, 0, win.width, ' ');
421 import core.stdc.stdio : snprintf;
422 auto cp = curpos;
423 char[512] buf = void;
424 auto sx = cx;
425 if (visualtabs) {
426 int ry;
427 lc.pos2xyVT(cp, sx, ry);
429 // mod, (pos), topline, linecount
430 auto len = snprintf(buf.ptr, buf.length, " %c[%04u:%05u : %5u : %5u] ",
431 (textChanged ? '*' : ' '), sx+0, cy+1, topline, linecount);
432 // character code (hex, dec)
433 if (!utfuck) {
434 auto c = cast(uint)gb[cp];
435 len += snprintf(buf.ptr+len, buf.length-len, "0x%02x %3u", c, c);
436 } else {
437 auto c = cast(uint)dcharAt(cp);
438 len += snprintf(buf.ptr+len, buf.length-len, "U%04x %5u", c, c);
440 // filesize, bufstart, bufend
441 len += snprintf(buf.ptr+len, buf.length-len, " [ %u : %d : %d]", gb.textsize, bstart, bend);
442 // counter
443 if (repeatCounter >= 0) {
444 len += snprintf(buf.ptr+len, buf.length-len, " rep:%d", repeatCounter);
445 if (repeatSP > 0) len += snprintf(buf.ptr+len, buf.length-len, " (%d)", repeatSP);
447 // done
448 if (len > winw) len = winw;
449 win.writeStrAt(0, 0, buf[0..len]);
450 // mode flags
451 if (utfuck) {
452 win.fb(11, 9);
453 if (readonly) win.writeCharsAt(0, 0, 1, '/'); else win.writeCharsAt(0, 0, 1, 'U');
454 } else if (readonly) {
455 win.fb(11, 9);
456 win.writeCharsAt(0, 0, 1, 'R');
460 // highlighting is done, other housekeeping is done, only draw
461 // lidx is always valid
462 // must repaint the whole line
463 // use `winXXX` vars to know window dimensions
464 //FIXME: clean this up!
465 public override void drawLine (int lidx, int yofs, int xskip) {
466 immutable vt = visualtabs;
467 immutable tabsz = lc.tabsize;
468 auto win = XtWindow(winx, winy, winw, winh);
469 auto pos = lc.linestart(lidx);
470 int x = -xskip;
471 int y = yofs;
472 auto ts = gb.textsize;
473 bool inBlock = (bstart < bend && pos >= bstart && pos < bend);
474 bool bookmarked = isLineBookmarked(lidx);
475 bool hasHL = (hl !is null); // do we have highlighter (just a cache)
476 if (singleline) {
477 win.color = (clrText ? clrText : TextColor);
478 if (killTextOnChar) win.color = (clrTextUnchanged ? clrTextUnchanged : TextKillColor);
479 if (inBlock) win.color = (clrBlock ? clrBlock : BlockColor);
480 } else {
481 win.color = (hasHL ? TextColor : TextColorNoHi);
482 if (bookmarked) win.color = BookmarkColor; else if (inBlock) win.color = BlockColor;
484 // if we have no highlighter, check for trailing spaces explicitly
485 // look for trailing spaces even if we have a highlighter
486 int trspos = lc.lineend(lidx); // this is where trailing spaces starts (will)
487 immutable lend = trspos;
488 if (!singleline) while (trspos > pos && gb[trspos-1] <= ' ') --trspos;
489 bool utfucked = utfuck;
490 int bs = bstart, be = bend;
491 auto sltextClr = (singleline ?
492 (killTextOnChar ?
493 (clrTextUnchanged ? clrTextUnchanged : TextKillColor) :
494 (clrText ? clrText : TextColor)) :
495 (hasHL ? TextColor : TextColorNoHi));
496 auto blkClr = (singleline ? (clrBlock ? clrBlock : BlockColor) : BlockColor);
497 while (pos <= lend) {
498 inBlock = (bs < be && pos >= bs && pos < be);
499 auto ch = gb[pos++];
500 if (ch == '\n') {
501 if (!killTextOnChar && !singleline) win.color = (hasHL ? hiColor(gb.hi(pos-1)) : TextColorNoHi); else win.color = sltextClr;
502 --pos;
503 break;
504 } else if (hasHL) {
505 // has highlighter
506 if (pos-1 >= trspos) {
507 if (ch != '\t') ch = '.';
508 if (!killTextOnChar && !singleline) win.color = (ch != '\t' ? TrailSpaceColor : VisualTabColor); else win.color = sltextClr;
509 } else if (ch < ' ' || ch == 127) {
510 if (!killTextOnChar && !singleline) win.color = (ch != '\t' ? BadColor : VisualTabColor); else win.color = sltextClr;
511 } else {
512 auto hs = gb.hi(pos-1);
513 if (!killTextOnChar && !singleline) win.color = hiColor(hs); else win.color = sltextClr;
515 } else {
516 // no highlighter
517 if (pos-1 >= trspos) {
518 if (ch != '\t') ch = '.';
519 if (!killTextOnChar && !singleline) win.color = (ch != '\t' ? TrailSpaceColor : VisualTabColor); else win.color = sltextClr;
520 } else if (ch < ' ' || ch == 127) {
521 if (!killTextOnChar && !singleline) win.color = (ch != '\t' ? BadColor : VisualTabColor); else win.color = sltextClr;
522 } else {
523 win.color = sltextClr;
526 if (!killTextOnChar && !singleline) {
527 if (bookmarked) win.color = BookmarkColor; else if (inBlock) win.color = blkClr;
528 } else {
529 if (inBlock) win.color = blkClr; else win.color = sltextClr;
531 if (x < winw) {
532 if (vt && ch == '\t') {
533 int ex = ((x+tabsz)/tabsz)*tabsz;
534 if (ex > 0) {
535 int sz = ex-x;
536 if (sz > 1) {
537 win.writeCharsAt(x, y, 1, '<');
538 win.writeCharsAt(x+1, y, sz-2, '-');
539 win.writeCharsAt(ex-1, y, 1, '>');
540 } else {
541 win.writeCharsAt(x, y, 1, '\t');
544 x = ex-1; // compensate the following increment
545 } else if (!utfucked) {
546 if (x >= 0) win.writeCharsAt(x, y, 1, recodeCharFrom(ch));
547 } else {
548 // utfuck
549 --pos;
550 if (x >= 0) {
551 dchar dch = dcharAt(pos);
552 if (dch > dchar.max || dch == 0xFFFD) {
553 auto oc = win.color;
554 scope(exit) win.color = oc;
555 if (!inBlock) win.color = UtfuckedColor;
556 win.writeCharsAt!true(x, y, 1, '\x7e'); // dot
557 } else {
558 win.writeCharsAt(x, y, 1, uni2koi(dch));
561 pos += gb.utfuckLenAt(pos);
563 if (++x >= winw) return;
566 if (x >= winw) return;
567 if (x < 0) x = 0;
568 if (pos >= lend) {
569 win.color = (hasHL || singleline ? TextColor : TextColorNoHi);
570 if (!killTextOnChar && !singleline) {
571 if (bookmarked) win.color = BookmarkColor; else win.color = sltextClr;
572 } else {
573 win.color = sltextClr;
575 if (bs < be && pos >= bs && pos < be) win.color = blkClr;
577 win.writeCharsAt(x, y, winw-x, ' ');
580 // just clear line
581 // use `winXXX` vars to know window dimensions
582 public override void drawEmptyLine (int yofs) {
583 auto win = XtWindow(winx, winy, winw, winh);
584 if (singleline) {
585 win.color = (clrText ? clrText : TextColor);
586 if (killTextOnChar) win.color = (clrTextUnchanged ? clrTextUnchanged : TextKillColor);
587 } else {
588 win.color = TextColor;
590 win.writeCharsAt(0, yofs, winw, ' ');
593 public override void drawPageBegin () {
596 // check if line has only spaces (or comments, use highlighter) to begin/end (determined by dir)
597 final bool lineHasOnlySpaces (int pos, int dir) {
598 if (dir == 0) return false;
599 dir = (dir < 0 ? -1 : 1);
600 auto ts = textsize;
601 if (ts == 0) return true; // wow, rare case
602 if (pos < 0) { if (dir < 0) return true; pos = 0; }
603 if (pos >= ts) { if (dir > 0) return true; pos = ts-1; }
604 while (pos >= 0 && pos < ts) {
605 auto ch = gb[pos];
606 if (ch == '\n') break;
607 if (ch > ' ') {
608 // nonspace, check highlighting, if any
609 if (hl !is null) {
610 auto lidx = lc.pos2line(pos);
611 if (hl.fixLine(lidx)) markLinesDirty(lidx, 1); // so it won't lost dirty flag in redraw
612 if (!hiIsComment(gb.hi(pos))) return false;
613 // ok, it is comment, it's the same as whitespace
614 } else {
615 return false;
618 pos += dir;
620 return true;
623 // always starts at BOL
624 final int lineFindFirstNonSpace (int pos) {
625 auto ts = textsize;
626 if (ts == 0) return 0; // wow, rare case
627 pos = lc.linestart(lc.pos2line(pos));
628 while (pos < ts) {
629 auto ch = gb[pos];
630 if (ch == '\n') break;
631 if (ch > ' ') {
632 // nonspace, check highlighting, if any
633 if (hl !is null) {
634 auto lidx = lc.pos2line(pos);
635 if (hl.fixLine(lidx)) markLinesDirty(lidx, 1); // so it won't lost dirty flag in redraw
636 if (!hiIsComment(gb.hi(pos))) return pos;
637 // ok, it is comment, it's the same as whitespace
638 } else {
639 return pos;
642 ++pos;
644 return pos;
647 final int drawHiBracket(bool dodraw=true) (int pos, int lidx, char bch, char ech, int dir, bool drawline=false) {
648 enum LineScanPages = 8;
649 int level = 1;
650 auto ts = gb.textsize;
651 auto stpos = pos;
652 int toplimit = (drawline ? mTopLine-winh*(LineScanPages-1) : mTopLine);
653 int botlimit = (drawline ? mTopLine+winh*LineScanPages : mTopLine+winh);
654 pos += dir;
655 while (pos >= 0 && pos < ts) {
656 auto ch = gb[pos];
657 if (ch == '\n') {
658 lidx += dir;
659 if (lidx < toplimit || lidx >= botlimit) return -1;
660 pos += dir;
661 continue;
663 if (isAnyTextChar(pos, (dir > 0))) {
664 if (ch == bch) {
665 ++level;
666 } else if (ch == ech) {
667 if (--level == 0) {
668 int rx, ry;
669 lc.pos2xyVT(pos, rx, ry);
670 if (rx >= mXOfs || rx < mXOfs+winw) {
671 static if (dodraw) {
672 if (ry >= mTopLine && ry < mTopLine+winh) {
673 auto win = XtWindow(winx, winy, winw, winh);
674 win.color = BracketColor;
675 win.writeCharsAt(rx-mXOfs, ry-mTopLine, 1, ech);
676 markLinesDirty(ry, 1);
678 // draw line with opening bracket if it is out of screen
679 if (drawline && dir < 0 && ry < mTopLine) {
680 // line start
681 int ls = pos;
682 while (ls > 0 && gb[ls-1] != '\n') --ls;
683 // skip leading spaces
684 while (ls < pos && gb[ls] <= ' ') ++ls;
685 // line end
686 int le = pos+1;
687 while (le < ts && gb[le] != '\n') ++le;
688 // remove trailing spaces
689 while (le > pos && gb[le-1] <= ' ') --le;
690 if (ls < le) {
691 auto win = XtWindow(winx+1, winy-1, winw-1, 1);
692 win.color = XtColorFB!(TtyRgb2Color!(0x40, 0x40, 0x40), TtyRgb2Color!(0xb2, 0xb2, 0xb2)); // 0,7
693 win.writeCharsAt(0, 0, winw, ' ');
694 int x = 0;
695 while (x < winw && ls < le) {
696 win.writeCharsAt(x, 0, 1, uni2koi(gb.uniAt(ls)));
697 ls += gb.utfuckLenAt(ls);
698 ++x;
702 if (drawline) {
703 // draw vertical line
704 int stx, sty, ls;
705 if (dir > 0) {
706 // opening
707 // has some text after the bracket on the starting line?
708 //if (!lineHasOnlySpaces(stpos+1, 1)) return; // has text, can't draw
709 // find first non-space at the starting line
710 ls = lineFindFirstNonSpace(stpos);
711 } else if (dir < 0) {
712 // closing
713 lc.pos2xyVT(stpos, rx, ry);
714 ls = lineFindFirstNonSpace(pos);
716 lc.pos2xyVT(ls, stx, sty);
717 if (stx == rx) {
718 markLinesDirtySE(sty+1, ry-1);
719 rx -= mXOfs;
720 stx -= mXOfs;
721 ry -= mTopLine;
722 sty -= mTopLine;
723 auto win = XtWindow(winx, winy, winw, winh);
724 win.color = VLineColor;
725 win.vline(stx, sty+1, ry-sty-1);
730 return pos;
734 pos += dir;
736 return -1;
739 protected final void drawPartHighlight (int pos, int count, uint clr) {
740 if (pos >= 0 && count > 0 && pos < gb.textsize) {
741 int rx, ry;
742 lc.pos2xyVT(pos, rx, ry);
743 //auto oldclr = xtGetColor;
744 //scope(exit) win.color = oldclr;
745 if (ry >= topline && ry < topline+winh) {
746 // visible, mark it
747 auto win = XtWindow(winx, winy, winw, winh);
748 win.color = clr;
749 if (count > gb.textsize-pos) count = gb.textsize-pos;
750 rx -= mXOfs;
751 ry -= topline;
752 //ry += winy;
753 if (!utfuck) {
754 foreach (immutable _; 0..count) {
755 if (rx >= 0 && rx < winw) win.writeCharsAt(rx, ry, 1, recodeCharFrom(gb[pos++]));
756 ++rx;
758 } else {
759 int end = pos+count;
760 while (pos < end) {
761 if (rx >= winw) break;
762 if (rx >= 0) {
763 dchar dch = dcharAt(pos);
764 if (dch > dchar.max || dch == 0xFFFD) {
765 //auto oc = xtGetColor();
766 //win.color = UtfuckedColor;
767 win.writeCharsAt!true(rx, ry, 1, '\x7e'); // dot
768 } else {
769 win.writeCharsAt(rx, ry, 1, uni2koi(dch));
772 ++rx;
773 pos += gb.utfuckLenAt(pos);
780 public override void drawPagePost () {
781 auto pos = curpos;
782 if (isAnyTextChar(pos, false)) {
783 auto ch = gb[pos];
784 if (ch == '(') drawHiBracket(pos, cy, ch, ')', 1);
785 else if (ch == '{') drawHiBracket(pos, cy, ch, '}', 1, true);
786 else if (ch == '[') drawHiBracket(pos, cy, ch, ']', 1);
787 else if (ch == ')') drawHiBracket(pos, cy, ch, '(', -1);
788 else if (ch == '}') drawHiBracket(pos, cy, ch, '{', -1, true);
789 else if (ch == ']') drawHiBracket(pos, cy, ch, '[', -1);
791 // highlight search
792 if (incSearchHitPos >= 0 && incSearchHitLen > 0 && incSearchHitPos < gb.textsize) {
793 drawPartHighlight(incSearchHitPos, incSearchHitLen, IncSearchColor);
795 drawScrollBar();
798 public override void drawPageEnd () {
799 if (mPromptActive) {
800 auto win = XtWindow(winx, winy-(singleline || hideStatus ? 0 : 1), winw, 1);
801 win.color = mPromptInput.clrText;
802 win.writeCharsAt(0, 0, winw, ' ');
803 win.writeStrAt(0, 0, mPromptPrompt[0..mPromptLen]);
804 win.writeCharsAt(mPromptLen, 0, 1, ':');
805 mPromptInput.fullDirty(); // force redraw
806 mPromptInput.drawPage();
807 return;
811 //TODO: fix cx if current line was changed
812 final void doUntabify (int tabSize=2) {
813 if (mReadOnly || gb.textsize == 0) return;
814 if (tabSize < 1 || tabSize > 255) return;
815 int pos = 0;
816 auto ts = gb.textsize;
817 int curx = 0;
818 while (pos < ts && gb[pos] != '\t') {
819 if (gb[pos] == '\n') curx = 0; else ++curx;
820 ++pos;
822 if (pos >= ts) return;
823 undoGroupStart();
824 scope(exit) undoGroupEnd();
825 char[255] spaces = ' ';
826 txchanged = true;
827 while (pos < ts) {
828 // replace space
829 assert(gb[pos] == '\t');
830 int spc = tabSize-(curx%tabSize);
831 replaceText!"none"(pos, 1, spaces[0..spc]);
832 // find next tab
833 ts = gb.textsize;
834 while (pos < ts && gb[pos] != '\t') {
835 if (gb[pos] == '\n') curx = 0; else ++curx;
836 ++pos;
839 normXY();
842 //TODO: fix cx if current line was changed
843 final void doRemoveTailingSpaces () {
844 if (mReadOnly || gb.textsize == 0) return;
845 bool wasChanged = false;
846 scope(exit) if (wasChanged) undoGroupEnd();
847 foreach (int lidx; 0..linecount) {
848 auto ls = lc.linestart(lidx);
849 auto le = lc.lineend(lidx); // points at '\n', or after text buffer
850 int count = 0;
851 while (le > ls && gb[le-1] <= ' ') { --le; ++count; }
852 if (count == 0) continue;
853 if (!wasChanged) { undoGroupStart(); wasChanged = true; }
854 deleteText!"none"(le, count);
856 normXY();
859 // not string, not comment
860 // if `goingDown` is true, update highlighting
861 final bool isAnyTextChar (int pos, bool goingDown) {
862 if (hl is null) return true;
863 if (pos < 0 || pos >= gb.textsize) return false;
864 // update highlighting
865 if (goingDown && hl !is null) {
866 auto lidx = lc.pos2line(pos);
867 if (hl.fixLine(lidx)) markLinesDirty(lidx, 1); // so it won't lost dirty flag in redraw
868 // ok, it is comment, it's the same as whitespace
870 switch (gb.hi(pos).kwtype) {
871 case HiCommentOneLine:
872 case HiCommentMulti:
873 case HiCommentDirective:
874 case HiChar:
875 case HiCharSpecial:
876 case HiDQString:
877 case HiDQStringSpecial:
878 case HiSQString:
879 case HiSQStringSpecial:
880 case HiBQString:
881 case HiRQString:
882 return false;
883 default:
885 return true;
888 final bool isInComment (int pos) {
889 if (hl is null || pos < 0 || pos >= gb.textsize) return false;
890 return hiIsComment(gb.hi(pos));
893 final bool isACGoodWordChar (int pos) {
894 if (pos < 0 || pos >= gb.textsize) return false;
895 if (!isWordChar(gb[pos])) return false;
896 if (hl !is null) {
897 // don't autocomplete in strings
898 switch (gb.hi(pos).kwtype) {
899 case HiNumber:
900 case HiChar:
901 case HiCharSpecial:
902 case HiDQString:
903 case HiDQStringSpecial:
904 case HiSQString:
905 case HiSQStringSpecial:
906 case HiBQString:
907 case HiRQString:
908 return false;
909 default:
912 return true;
915 const(char)[] delegate (EditorEngine ed, const(char)[] tk, int tkpos) completeToken;
917 final void doAutoComplete () {
918 scope(exit) {
919 acused = 0;
920 if (acbuffer.length > 0) {
921 acbuffer.length = 0;
922 acbuffer.assumeSafeAppend;
926 void addAcToken (const(char)[] tk) {
927 if (tk.length == 0) return;
928 foreach (const(char)[] t; aclist[0..acused]) if (t == tk) return;
929 if (acused >= aclist.length) return;
930 // add to buffer
931 auto pos = acbuffer.length;
932 acbuffer ~= tk;
933 aclist[acused++] = acbuffer[pos..$];
936 import std.ascii : isAlphaNum;
937 // get token to autocomplete
938 auto pos = curpos;
939 if (!isACGoodWordChar(pos-1)) return;
940 //debug(egauto) { { import iv.vfs; auto fo = VFile("z00_ch.bin", "w"); fo.write(ch); } }
941 bool startedInComment = isInComment(pos-1);
942 char[128] tk = void;
943 int tkpos = cast(int)tk.length;
944 while (pos > 0 && isACGoodWordChar(pos-1)) {
945 if (tkpos == 0) return;
946 tk.ptr[--tkpos] = gb[--pos];
948 int tklen = cast(int)tk.length-tkpos;
949 auto tkstpos = pos;
950 //HACK: try "std.algo"
951 if (gb[pos-1] == '.' && gb[pos-2] == 'd' && gb[pos-3] == 't' && gb[pos-4] == 's' && !isACGoodWordChar(pos-5)) {
952 if (tk[$-tklen..$] == "algo") {
953 tkstpos += tklen;
954 string ntx = "rithm";
955 // insert new token
956 replaceText!"end"(tkstpos, 0, ntx);
957 return;
960 //HACK: try "arsd.sdpy"
961 if (gb[pos-1] == '.' && gb[pos-2] == 'd' && gb[pos-3] == 's' && gb[pos-4] == 'r' && gb[pos-5] == 'a' && !isACGoodWordChar(pos-6)) {
962 if (tk[$-tklen..$] == "sdpy") {
963 string ntx = "simpledisplay";
964 // insert new token
965 replaceText!"end"(tkstpos, tklen, ntx);
966 return;
969 //debug(egauto) { { import iv.vfs; auto fo = VFile("z00_tk.bin", "w"); fo.write(tk[$-tklen..$]); } }
970 // build token list
971 char[128] xtk = void;
972 while (pos > 0) {
973 while (pos > 0 && !isACGoodWordChar(pos-1)) --pos;
974 if (pos <= 0) break;
975 int xtp = cast(int)xtk.length;
976 while (pos > 0 && isACGoodWordChar(pos-1)) {
977 if (xtp > 0) {
978 xtk.ptr[--xtp] = gb[--pos];
979 } else {
980 xtp = -1;
981 --pos;
984 if (xtp >= 0 && isInComment(pos) == startedInComment) {
985 int xlen = cast(int)xtk.length-xtp;
986 if (xlen > tklen) {
987 import core.stdc.string : memcmp;
988 if (memcmp(xtk.ptr+xtk.length-xlen, tk.ptr+tk.length-tklen, tklen) == 0) {
989 const(char)[] tt = xtk[$-xlen..$];
990 addAcToken(tt);
991 if (acused >= 128) break;
996 debug(egauto) { { import iv.vfs; auto fo = VFile("z00_list.bin", "w"); fo.writeln(list[]); } }
998 const(char)[] getDefToken () /*nothrow @trusted @nogc*/ {
999 if (completeToken !is null) return completeToken(this, tk[$-tklen..$], tkpos);
1001 switch (tk[$-tklen..$]) {
1002 case "ab": return "abstract";
1003 case "br": return "break";
1004 case "ch": return "char";
1005 case "co": return "continue";
1006 case "de": return "delegate";
1007 case "fa": return "false";
1008 case "fi": return "final";
1009 case "for": return "foreach";
1010 case "fu": return "function";
1011 case "imm": return "immutable";
1012 case "not": return "nothrow";
1013 case "noth": return "nothrow";
1014 case "ov": return "override";
1015 case "pro": return "property";
1016 case "st": return "string";
1017 case "tru": return (tkpos == 0 || gb[tkpos-1] != '@' ? "true" : "trusted");
1018 case "ub": return "ubyte";
1019 case "ui": return "uint";
1020 case "ul": return "ulong";
1021 case "us": return "ushort";
1022 case "ush": return "ushort";
1023 case "vo": return "void";
1024 // pascal
1026 case "ra": return "raise";
1027 case "Ex": return "Exception";
1028 case "Cr": return "Create";
1029 case "Cre": return "Create";
1030 case "De": return "Destroy";
1031 case "ove": return "overload";
1032 case "ovr": return "override";
1033 case "inh": return "inherited";
1034 case "inhe": return "inherited";
1035 case "proc": return "procedure";
1036 case "fun": return "function";
1037 case "con": return "constructor";
1038 case "des": return "destructor";
1040 default:
1043 return null;
1046 const(char)[] acp;
1047 if (acused == 0) {
1048 // try some common things
1049 acp = getDefToken();
1050 if (acp.length == 0) return;
1051 //{ import iv.vfs; auto fo = VFile("z00_tk.bin", "w"); fo.writeln(tk[$-tklen..$]); fo.writeln(acp); }
1052 } else if (acused == 1) {
1053 acp = aclist[0];
1054 auto tkx = getDefToken();
1055 if (tkx.length && tkx != acp) addAcToken(tkx);
1057 if (acused > 1) {
1058 int rx, ry;
1059 lc.pos2xyVT(tkstpos, rx, ry);
1060 //int residx = promptSelect(aclist[0..acused], winx+(rx-mXOfs), winy+(ry-mTopLine)+1);
1061 int residx = dialogSelectAC(aclist[0..acused], winx+(rx-mXOfs), winy+(ry-mTopLine)+1);
1062 if (residx < 0) return;
1063 acp = aclist[residx];
1065 if (mReadOnly) return;
1066 replaceText!"end"(tkstpos, tklen, acp);
1069 final char[] buildHelpText(this ME) () {
1070 char[] res;
1071 void buildHelpFor(UDA) () {
1072 foreach (string memn; __traits(allMembers, ME)) {
1073 static if (is(typeof(__traits(getMember, ME, memn)))) {
1074 foreach (const attr; __traits(getAttributes, __traits(getMember, ME, memn))) {
1075 static if (is(typeof(attr) == UDA)) {
1076 static if (!attr.hidden && attr.help.length && attr.key.length) {
1077 // check modifiers
1078 bool goodMode = true;
1079 foreach (const attrx; __traits(getAttributes, __traits(getMember, ME, memn))) {
1080 static if (is(attrx == TEDSingleOnly)) { if (!singleline) goodMode = false; }
1081 else static if (is(attrx == TEDMultiOnly)) { if (singleline) goodMode = false; }
1082 else static if (is(attrx == TEDEditOnly)) { if (readonly) goodMode = false; }
1083 else static if (is(attrx == TEDROOnly)) { if (!readonly) goodMode = false; }
1085 if (goodMode) {
1086 //res ~= "|";
1087 res ~= attr.key;
1088 foreach (immutable _; attr.key.length..12) res ~= ".";
1089 //res ~= "|";
1090 res ~= " ";
1091 res ~= attr.help;
1092 res ~= "\n";
1100 buildHelpFor!TEDKey;
1101 while (res.length && res[$-1] <= ' ') res = res[0..$-1];
1102 return res;
1105 protected enum Ecc { None, Eaten, Combo }
1107 // None: not valid
1108 // Eaten: exact hit
1109 // Combo: combo start
1110 // comboBuf should contain comboCount+1 keys!
1111 protected final Ecc checkKeys (const(char)[] keys) {
1112 TtyEvent k;
1113 // check if current combo prefix is ok
1114 foreach (const ref TtyEvent ck; comboBuf[0..comboCount+1]) {
1115 keys = TtyEvent.parse(k, keys);
1116 if (k.key == TtyEvent.Key.Error || k.key == TtyEvent.Key.None || k.key == TtyEvent.Key.Unknown) return Ecc.None;
1117 if (k != ck) return Ecc.None;
1119 return (keys.length == 0 ? Ecc.Eaten : Ecc.Combo);
1122 // fuck! `(this ME)` trick doesn't work here
1123 protected final Ecc doEditorCommandByUDA(ME=typeof(this)) (TtyEvent key) {
1124 import std.traits;
1125 if (key.key == TtyEvent.Key.None) return Ecc.None;
1126 if (key.key == TtyEvent.Key.Error || key.key == TtyEvent.Key.Unknown) { repeatCounter = -1; comboCount = 0; return Ecc.None; }
1127 // process repeat counter
1128 if (repeatCounter >= 0) {
1129 void pushVal () {
1130 if (repeatSP < repeatStack.length) {
1131 repeatStack[repeatSP++] = repeatCounter;
1132 } else {
1133 foreach (immutable idx; 1..repeatStack.length) repeatStack[idx-1] = repeatStack[idx];
1134 repeatStack[$-1] = repeatCounter;
1136 repeatCounter = 1;
1137 repeatMode = RepMode.JustStarted;
1139 // TOS op RC
1140 void doBinMath(string op) () {
1141 if (repeatSP < 1) { ttyBeep(); return; }
1142 static if (op == "+") repeatCounter = repeatStack[repeatSP-1]+repeatCounter;
1143 else static if (op == "-") repeatCounter = repeatStack[repeatSP-1]-repeatCounter;
1144 else static if (op == "*") repeatCounter = repeatStack[repeatSP-1]*repeatCounter;
1145 else static if (op == "/") { if (repeatCounter == 0) { ttyBeep(); repeatCounter = 0; } else repeatCounter = repeatStack[repeatSP-1]/repeatCounter; }
1146 else static if (op == "%") { if (repeatCounter == 0) { ttyBeep(); repeatCounter = 0; } else repeatCounter = repeatStack[repeatSP-1]%repeatCounter; }
1147 else static assert(0, "wtf?!");
1148 repeatMode = RepMode.Normal;
1149 --repeatSP; // pop used value
1151 // cancel counter mode?
1152 if (key == "C-C" || key == "C-G") { repeatCounter = -1; return Ecc.Eaten; }
1153 if (repeatMode != RepMode.CSpace) {
1154 if (key == "C-Space") { repeatMode = RepMode.CSpace; return Ecc.None; } // so C-Space can allow insert alot of numbers, for example
1155 // digit?
1156 if (key.key == TtyEvent.Key.Char && key.ch >= '0' && key.ch <= '9') {
1157 if (repeatMode == RepMode.JustStarted) repeatCounter = 0;
1158 repeatCounter = repeatCounter*10+key.ch-'0';
1159 repeatMode = RepMode.Normal;
1160 return Ecc.Combo;
1162 // math op?
1163 if (key.key == TtyEvent.Key.Char && key.ch == '+') { doBinMath!"+"(); return Ecc.Combo; }
1164 if (key.key == TtyEvent.Key.Char && key.ch == '-') { doBinMath!"-"(); return Ecc.Combo; }
1165 if (key.key == TtyEvent.Key.Char && key.ch == '*') { doBinMath!"*"(); return Ecc.Combo; }
1166 if (key.key == TtyEvent.Key.Char && key.ch == '/') { doBinMath!"/"(); return Ecc.Combo; }
1167 if (key.key == TtyEvent.Key.Char && key.ch == '%') { doBinMath!"%"(); return Ecc.Combo; }
1168 // pop or drop value
1169 if (key == "Backspace" || key == "M-Backspace") {
1170 if (repeatSP == 0) { ttyBeep(); repeatCounter = 1; repeatMode = RepMode.JustStarted; return Ecc.Combo; }
1171 if (key == "Backspace") repeatCounter = repeatStack[repeatSP-1];
1172 --repeatSP;
1173 return Ecc.Combo;
1175 // push value
1176 if (key == "=" || key == "!") {
1177 pushVal();
1178 return Ecc.Combo;
1180 } else {
1181 repeatMode = RepMode.Normal;
1184 // normal key processing
1185 bool possibleCombo = false;
1186 // temporarily add current key to combo
1187 comboBuf[comboCount] = key;
1188 // check all known combos
1189 foreach (string memn; __traits(allMembers, ME)) {
1190 static if (is(typeof(&__traits(getMember, ME, memn)))) {
1191 import std.meta : AliasSeq;
1192 alias mx = AliasSeq!(__traits(getMember, ME, memn))[0];
1193 static if (isCallable!mx && hasUDA!(mx, TEDKey)) {
1194 // check modifiers
1195 bool goodMode = true;
1196 static if (hasUDA!(mx, TEDSingleOnly)) { if (!singleline) goodMode = false; }
1197 static if (hasUDA!(mx, TEDMultiOnly)) { if (singleline) goodMode = false; }
1198 static if (hasUDA!(mx, TEDEditOnly)) { if (readonly) goodMode = false; }
1199 static if (hasUDA!(mx, TEDROOnly)) { if (!readonly) goodMode = false; }
1200 if (goodMode) {
1201 foreach (const TEDKey attr; getUDAs!(mx, TEDKey)) {
1202 auto cc = checkKeys(attr.key);
1203 if (cc == Ecc.Eaten) {
1204 // hit
1205 static if (is(ReturnType!mx == void)) {
1206 comboCount = 0; // reset combo
1207 static if (hasUDA!(mx, TEDRepeated)) {
1208 scope(exit) { repeatCounter = -1; repeatIteration = 0; } // reset counter
1209 if (repeatCounter < 0) repeatCounter = 1; // no counter means 1
1210 foreach (immutable rc; 0..repeatCounter) { repeatIteration = rc; mx(); }
1211 } else {
1212 static if (!hasUDA!(mx, TEDRepeatChar)) repeatCounter = -1;
1213 mx();
1215 return Ecc.Eaten;
1216 } else {
1217 repeatIteration = 0;
1218 static if (hasUDA!(mx, TEDRepeated)) {
1219 if (repeatCounter != 0 && mx()) {
1220 scope(exit) { repeatCounter = -1; repeatIteration = 0; } // reset counter
1221 if (repeatCounter < 0) repeatCounter = 1; // no counter means 1
1222 comboCount = 0; // reset combo
1223 foreach (immutable rc; 1..repeatCounter) {
1224 repeatIteration = rc;
1225 if (!mx()) break;
1227 return Ecc.Eaten;
1229 } else {
1230 static if (!hasUDA!(mx, TEDRepeatChar)) repeatCounter = -1;
1231 if (mx()) {
1232 repeatCounter = -1; // reset counter
1233 comboCount = 0; // reset combo
1234 return Ecc.Eaten;
1238 } else if (cc == Ecc.Combo) {
1239 possibleCombo = true;
1246 // check if we can start/continue combo
1247 // combo can't start with normal char, but can include normal chars
1248 if (possibleCombo && (comboCount > 0 || key.key != TtyEvent.Key.Char)) {
1249 if (++comboCount < comboBuf.length-1) return Ecc.Combo;
1251 // if we have combo prefix, eat key unconditionally
1252 if (comboCount > 0) {
1253 repeatCounter = -1; // reset counter
1254 comboCount = 0; // reset combo, too long, or invalid, or none
1255 return Ecc.Eaten;
1257 return Ecc.None;
1260 void doCounterMode () {
1261 if (waitingInF5) { repeatCounter = -1; return; }
1262 if (repeatCounter < 0) {
1263 // start counter mode
1264 repeatCounter = 1;
1265 repeatMode = RepMode.JustStarted; // just started
1266 } else {
1267 repeatCounter *= 2; // so ^P will multiply current counter
1271 char[] pasteCollector;
1272 int pasteModeCounter;
1273 bool pastePrevWasCR;
1275 private void promptKeyProcessor (EditorEngine ed) {
1276 // input buffer changed
1277 // check if it was *really* changed
1278 if (incSearchBuf.length == ed.textsize) {
1279 int pos = 0;
1280 foreach (char ch; ed[]) { if (incSearchBuf[pos] != ch) break; ++pos; }
1281 if (pos >= ed.textsize) return; // nothing was changed, so nothing to do
1283 // recollect it
1284 incSearchBuf.length = 0;
1285 incSearchBuf.assumeSafeAppend;
1286 foreach (char ch; ed[]) incSearchBuf ~= ch;
1287 doNextIncSearch(false); // don't move pointer
1290 void addToPasteCollector (TtyEvent key) {
1291 void addChar (dchar dch) {
1292 if (dch < ' ') { if (dch != '\t' && dch != '\n') return; }
1293 if (dch > 255) return;
1294 char[4] buf = void;
1295 int len;
1296 if (utfuck) {
1297 len = utf8Encode(buf[], koi2uni(cast(char)dch));
1298 if (len < 1) return;
1299 } else {
1300 buf[0] = cast(char)dch;
1301 len = 1;
1303 foreach (char pch; buf[0..len]) {
1304 auto optr = pasteCollector.ptr;
1305 pasteCollector ~= pch;
1306 if (pasteCollector.ptr !is optr) {
1307 import core.memory : GC;
1308 if (pasteCollector.ptr is GC.addrOf(pasteCollector.ptr)) GC.setAttr(pasteCollector.ptr, GC.BlkAttr.NO_INTERIOR);
1313 repeatCounter = -1;
1314 if (key.key == TtyEvent.Key.PasteStart) {
1315 //VFile("z00.log", "w").writeln("PasteStart; pasteModeCounter before: ", pasteModeCounter);
1316 ++pasteModeCounter;
1317 pastePrevWasCR = false;
1318 return;
1320 if (key.key == TtyEvent.Key.PasteEnd) {
1321 //VFile("z00.log", "w").writeln("PasteEnd; pasteModeCounter before: ", pasteModeCounter);
1322 pastePrevWasCR = false;
1323 if (pasteModeCounter < 1) { ttyBeep(); ttyBeep(); return; }
1324 if (--pasteModeCounter == 0) {
1325 if (pasteCollector.length) {
1326 // insert text in "paste mode"
1327 if (incInputActive) {
1328 foreach (char ch; pasteCollector) {
1329 TtyEvent k;
1330 k.key = TtyEvent.Key.Char;
1331 k.ch = ch;
1332 promptProcessKey(k, &promptKeyProcessor);
1334 drawStatus(); // just in case
1335 } else {
1336 doPasteStart();
1337 scope(exit) doPasteEnd();
1338 if (utfuck) doPutTextUtf(pasteCollector); else doPutText(pasteCollector);
1340 pasteCollector.length = 0;
1341 pasteCollector.assumeSafeAppend;
1342 //ttyBeep();
1345 return;
1347 if (key == "Enter") {
1348 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "{Enter}"); }
1349 if (!pastePrevWasCR) addChar('\n');
1350 pastePrevWasCR = false;
1351 return;
1353 if (key.key == TtyEvent.Key.ModChar && (key.ch == 13 || key.ch == 10 || key.ch == 12)) {
1354 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "{X:%d}", cast(int)key.ch); }
1355 if (key.ch != 13 || !pastePrevWasCR) addChar('\n');
1356 pastePrevWasCR = (key.ch == 13);
1357 return;
1359 if (key.key == TtyEvent.Key.ModChar) {
1360 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "{C:%d:%c}", cast(int)key.ch, cast(int)key.ch); }
1361 pastePrevWasCR = false;
1362 return;
1364 pastePrevWasCR = false;
1365 if (key.key == TtyEvent.Key.Char) {
1366 //{ import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "%c", cast(int)key.ch); }
1367 addChar(cast(char)key.ch);
1368 return;
1372 bool processKey (TtyEvent key) {
1373 //{ import core.stdc.stdio : stderr, fprintf; auto st = key.toString(); fprintf(stderr, "{%.*s}", cast(uint)st.length, st.ptr); }
1374 // hack it here, so it won't interfere with normal keyboard processing
1375 if (pasteModeCounter) { addToPasteCollector(key); return true; }
1376 if (key.key == TtyEvent.Key.PasteStart) { /*doPasteStart();*/ addToPasteCollector(key); return true; }
1377 if (key.key == TtyEvent.Key.PasteEnd) { /*doPasteEnd();*/ addToPasteCollector(key); return true; }
1379 if (waitingInF5) {
1380 waitingInF5 = false;
1381 if (key == "enter") {
1382 if (tempBlockFileName.length) {
1383 try { doBlockRead(tempBlockFileName); } catch (Exception) {} // sorry
1386 return true;
1389 if (key.key == TtyEvent.Key.Error || key.key == TtyEvent.Key.Unknown) { repeatCounter = -1; comboCount = 0; return false; }
1391 if (incInputActive) {
1392 if (key.key == TtyEvent.Key.ModChar) {
1393 if (key == "C-C") { incInputActive = false; resetIncSearchPos(); promptNoKillText(); return true; }
1394 if (key == "C-R") { incSearchDir = 1; doNextIncSearch(); promptNoKillText(); return true; }
1395 if (key == "C-V") { incSearchDir = -1; doNextIncSearch(); promptNoKillText(); return true; }
1396 //return true;
1398 if (key == "esc" || key == "enter") {
1399 incInputActive = false;
1400 promptDeactivate();
1401 resetIncSearchPos();
1402 return true;
1404 if (mPromptInput !is null) mPromptInput.utfuck = utfuck;
1405 promptProcessKey(key, &promptKeyProcessor);
1406 drawStatus(); // just in case
1407 return true;
1410 resetIncSearchPos();
1412 final switch (doEditorCommandByUDA(key)) {
1413 case Ecc.None: break;
1414 case Ecc.Combo:
1415 case Ecc.Eaten:
1416 return true;
1419 if (key.key == TtyEvent.Key.Char) {
1420 scope(exit) repeatCounter = -1; // reset counter
1421 if (readonly) return false;
1422 if (repeatCounter != 0) {
1423 if (repeatCounter < 0) repeatCounter = 1;
1424 if (repeatCounter > 1) {
1425 undoGroupStart();
1426 scope(exit) undoGroupEnd();
1427 foreach (immutable _; 0..repeatCounter) doPutChar(cast(char)key.ch);
1428 } else {
1429 doPutChar(cast(char)key.ch);
1432 return true;
1435 return false;
1438 bool processClick (int button, int x, int y) {
1439 if (x < 0 || y < 0 || x >= winw || y >= winh) return false;
1440 if (button != 0) return false;
1441 gotoXY(x, topline+y);
1442 return true;
1445 final:
1446 void pasteToX11 () {
1447 import std.file;
1448 import std.process;
1449 import std.regex;
1450 import std.stdio;
1452 if (!hasMarkedBlock) return;
1454 void doPaste (string cbkey) {
1455 try {
1456 string[string] moreEnv;
1457 moreEnv["K8_SUBSHELL"] = "tan";
1458 auto pp = std.process.pipeProcess(
1459 //["dmd", "-c", "-o-", "-verrors=64", "-vcolumns", fname],
1460 ["xsel", "-i", cbkey],
1461 std.process.Redirect.stderrToStdout|std.process.Redirect.stdout|std.process.Redirect.stdin,
1462 moreEnv,
1463 std.process.Config.none,
1464 null, //workdir
1466 pp.stdout.close();
1467 auto rng = markedBlockRange;
1468 foreach (char ch; rng) pp.stdin.write(ch);
1469 pp.stdin.flush();
1470 pp.stdin.close();
1471 pp.pid.wait;
1472 } catch (Exception) {}
1475 doPaste("-p");
1476 doPaste("-s");
1477 doPaste("-b");
1480 static struct PlainMatch {
1481 int s, e;
1482 @property bool empty () const pure nothrow @safe @nogc { return (s < 0 || s >= e); }
1485 // epos is not included
1486 final PlainMatch findTextPlain (const(char)[] pat, int spos, int epos, bool words, bool caseSens) {
1487 PlainMatch res;
1488 immutable ts = gb.textsize;
1489 if (pat.length == 0 || pat.length > ts) return res;
1490 if (epos <= spos || spos >= ts) return res;
1491 if (spos < 0) spos = 0;
1492 if (epos < 0) epos = 0;
1493 if (epos > ts) epos = ts;
1494 immutable bl = cast(int)pat.length;
1495 //dialogMessage!"debug"("findTextPlain", "spos=%s; epos=%s; ts=%s; curpos=%s; pat:[%s]", spos, epos, ts, curpos, pat);
1496 while (ts-spos >= bl) {
1497 if (caseSens || !pat.ptr[0].isalpha) {
1498 spos = gb.fastFindChar(spos, pat.ptr[0]);
1499 if (ts-spos < bl) break;
1501 bool found = true;
1502 if (caseSens) {
1503 foreach (int p; spos..spos+bl) if (gb[p] != pat.ptr[p-spos]) { found = false; break; }
1504 } else {
1505 foreach (int p; spos..spos+bl) if (gb[p].tolower != pat.ptr[p-spos].tolower) { found = false; break; }
1507 // check word boundaries
1508 if (found && words) {
1509 if (spos > 0 && isWordChar(gb[spos-1])) found = false;
1510 int ep = spos+bl;
1511 if (ep < ts && isWordChar(gb[ep])) found = false;
1513 //dialogMessage!"debug"("findTextPlain", "spos=%s; epos=%s; found=%s", spos, epos, found);
1514 if (found) {
1515 res.s = spos;
1516 res.e = spos+bl;
1517 break;
1519 ++spos;
1521 return res;
1524 // epos is not included
1525 final PlainMatch findTextPlainBack (const(char)[] pat, int spos, int epos, bool words, bool caseSens) {
1526 PlainMatch res;
1527 immutable ts = gb.textsize;
1528 if (pat.length == 0 || pat.length > ts) return res;
1529 if (epos <= spos || spos >= ts) return res;
1530 if (spos < 0) spos = 0;
1531 if (epos < 0) epos = 0;
1532 if (epos > ts) epos = ts;
1533 immutable bl = cast(int)pat.length;
1534 if (ts-epos < bl) epos = ts-bl;
1535 while (epos >= spos) {
1536 bool found = true;
1537 if (caseSens) {
1538 foreach (int p; epos..epos+bl) if (gb[p] != pat.ptr[p-epos]) { found = false; break; }
1539 } else {
1540 foreach (int p; epos..epos+bl) if (gb[p].tolower != pat.ptr[p-epos].tolower) { found = false; break; }
1542 if (found && words) {
1543 if (epos > 0 && isWordChar(gb[epos-1])) found = false;
1544 int ep = epos+bl;
1545 if (ep < ts && isWordChar(gb[ep])) found = false;
1547 if (found) {
1548 res.s = epos;
1549 res.e = epos+bl;
1550 break;
1552 --epos;
1554 return res;
1557 // epos is not included
1558 // caps are fixed so it can be used to index gap buffer
1559 final bool findTextRegExp (RegExp re, int spos, int epos, Pike.Capture[] caps) {
1560 Pike.Capture[1] tcaps;
1561 if (epos <= spos || spos >= textsize) return false;
1562 if (caps.length == 0) caps = tcaps;
1563 if (spos < 0) spos = 0;
1564 if (epos < 0) epos = 0;
1565 if (epos > textsize) epos = textsize;
1566 auto ctx = Pike.create(re, caps);
1567 if (!ctx.valid) return false;
1568 int res = SRes.Again;
1569 foreach (const(char)[] buf; gb.bufparts(spos)) {
1570 res = ctx.exec(buf, false);
1571 if (res < 0) {
1572 if (res != SRes.Again) return false;
1573 } else {
1574 break;
1577 if (res < 0) return false;
1578 if (spos+caps[0].s >= epos) return false;
1579 if (spos+caps[0].e > epos) return false;
1580 foreach (ref cp; caps) if (cp.s < cp.e) { cp.s += spos; cp.e += spos; }
1581 return true;
1585 public static struct FindResult {
1586 int line, col;
1589 // find all occurences of regexp, line by line (i.e. only first hit in line will be used)
1590 // returns "unsafe array" that cannot be modified
1591 final FindResult[] findAllRegExp (const(char)[] restr, bool* error=null) {
1592 FindResult[] res;
1594 void addFindRect (int line, int col) {
1595 if (res.length > 0 && res.ptr[0].line == line) return; // skip duplicate lines
1596 auto optr = res.ptr;
1597 res ~= FindResult(line, col);
1598 if (res.ptr !is optr) {
1599 import core.memory : GC;
1600 if (res.ptr is GC.addrOf(res.ptr)) GC.setAttr(res.ptr, GC.BlkAttr.NO_INTERIOR);
1604 auto re = RegExp.create(restr, SRFlags.Multiline);
1605 if (!re.valid) { if (error !is null) *error = true; return null; } // alas
1606 if (error !is null) *error = false;
1608 Pike.Capture[2] caps;
1609 int spos = 0;
1610 while (spos < textsize) {
1611 if (findTextRegExp(re, spos, textsize, caps[])) {
1612 int cx, cy;
1613 lc.pos2xy(caps.ptr[0].s, cx, cy);
1614 addFindRect(cy, cx);
1615 spos = caps[0].e;
1616 } else {
1617 break;
1621 return res;
1625 // epos is not included
1626 // caps are fixed so it can be used to index gap buffer
1627 final bool findTextRegExpBack (RegExp re, int spos, int epos, Pike.Capture[] caps) {
1628 import core.stdc.string : memcpy;
1629 MemPool csave;
1630 Pike.Capture* savedCaps;
1631 Pike.Capture[1] tcaps;
1632 if (epos <= spos || spos >= textsize) return false;
1633 if (caps.length == 0) caps = tcaps;
1634 if (spos < 0) spos = 0;
1635 if (epos < 0) epos = 0;
1636 if (epos > textsize) epos = textsize;
1637 while (spos < epos) {
1638 auto ctx = Pike.create(re, caps);
1639 if (!ctx.valid) break;
1640 int res = SRes.Again;
1641 foreach (const(char)[] buf; gb.bufparts(spos)) {
1642 res = ctx.exec(buf, false);
1643 if (res < 0) {
1644 if (res != SRes.Again) return false;
1645 } else {
1646 break;
1649 if (spos+caps[0].s >= epos) break;
1650 //dialogMessage!"debug"("findTextRegexpBack", "spos=%s; epos=%s; found=%s", spos, epos, spos+caps[0].s);
1651 // save this hit
1652 if (savedCaps is null) {
1653 if (!csave.active) {
1654 csave = MemPool.create;
1655 if (!csave.active) return false; // alas
1656 savedCaps = csave.alloc!(typeof(caps[0]))(cast(uint)(caps[0].sizeof*(caps.length-1)));
1657 if (savedCaps is null) return false; // alas
1660 // fix it
1661 foreach (ref cp; caps) if (cp.s < cp.e) { cp.s += spos; cp.e += spos; }
1662 memcpy(savedCaps, caps.ptr, caps[0].sizeof*caps.length);
1663 //FIXME: should we skip the whole found match?
1664 spos = caps[0].s+1;
1666 if (savedCaps is null) return false;
1667 // restore latest match
1668 memcpy(caps.ptr, savedCaps, caps[0].sizeof*caps.length);
1669 return true;
1672 final void srrPlain (in ref SearchReplaceOptions srr) {
1673 if (srr.search.length == 0) return;
1674 if (srr.inselection && !hasMarkedBlock) return;
1675 scope(exit) fullDirty(); // to remove highlighting
1676 bool closeGroup = false;
1677 scope(exit) if (closeGroup) undoGroupEnd();
1678 bool doAll = false;
1679 // epos will be fixed on replacement
1680 int spos, epos;
1681 if (srr.inselection) {
1682 spos = bstart;
1683 epos = bend;
1684 } else {
1685 if (!srr.backwards) {
1686 // forward
1687 spos = curpos;
1688 epos = textsize;
1689 } else {
1690 // backward
1691 spos = 0;
1692 epos = curpos;
1695 int count = 0;
1696 while (spos < epos) {
1697 PlainMatch mt;
1698 if (srr.backwards) {
1699 mt = findTextPlainBack(srr.search, spos, epos, srr.wholeword, srr.casesens);
1700 } else {
1701 mt = findTextPlain(srr.search, spos, epos, srr.wholeword, srr.casesens);
1703 if (mt.empty) break;
1704 // if i need to skip comments, do highlighting
1705 if (srr.nocomments && hl !is null) {
1706 auto lidx = lc.pos2line(mt.s);
1707 if (hl.fixLine(lidx)) markLinesDirty(lidx, 1); // so it won't lost dirty flag in redraw
1708 if (hiIsComment(gb.hi(mt.s))) {
1709 // skip
1710 if (!srr.backwards) spos = mt.s+1; else epos = mt.s;
1711 continue;
1714 // i found her!
1715 if (!doAll) {
1716 bool doundo = (mt.s != curpos);
1717 if (doundo) gotoPos!true(mt.s);
1718 fullDirty();
1719 drawPage();
1720 drawPartHighlight(mt.s, mt.e-mt.s, IncSearchColor);
1721 auto act = dialogReplacePrompt(lc.pos2line(mt.s)-topline+winy);
1722 //FIXME: do undo only if it was succesfully registered
1723 //doUndo(); // undo cursor movement
1724 if (act == DialogRepPromptResult.Cancel) break;
1725 //if (doundo) doUndo();
1726 if (act == DialogRepPromptResult.Skip) {
1727 if (!srr.backwards) spos = mt.s+1; else epos = mt.s;
1728 continue;
1730 if (act == DialogRepPromptResult.All) { doAll = true; }
1732 if (doAll && !closeGroup) { undoGroupStart(); closeGroup = true; }
1733 replaceText!"end"(mt.s, mt.e-mt.s, srr.replace);
1734 // fix search range
1735 if (!srr.backwards) {
1736 // forward
1737 spos = mt.s+cast(int)srr.replace.length;
1738 epos -= (mt.e-mt.s)-cast(int)srr.replace.length;
1739 } else {
1740 epos = mt.s;
1742 if (doAll) ++count;
1744 if (doAll) {
1745 fullDirty();
1746 drawPage();
1747 dialogMessage!"info"("Search and Replace", "%s replacement%s made", count, (count != 1 ? "s" : ""));
1751 final void srrRegExp (in ref SearchReplaceOptions srr) {
1752 import std.utf : byChar;
1753 if (srr.search.length == 0) return;
1754 if (srr.inselection && !hasMarkedBlock) return;
1755 auto re = RegExp.create(srr.search.byChar, (srr.casesens ? 0 : SRFlags.CaseInsensitive)|SRFlags.Multiline);
1756 if (!re.valid) { ttyBeep; return; }
1757 scope(exit) fullDirty(); // to remove highlighting
1758 bool closeGroup = false;
1759 scope(exit) if (closeGroup) undoGroupEnd();
1760 bool doAll = false;
1761 // epos will be fixed on replacement
1762 int spos, epos;
1764 if (srr.inselection) {
1765 spos = bstart;
1766 epos = bend;
1767 } else {
1768 if (!srr.backwards) {
1769 // forward
1770 spos = curpos;
1771 epos = textsize;
1772 } else {
1773 // backward
1774 spos = 0;
1775 epos = curpos;
1778 Pike.Capture[64] caps; // max captures
1780 char[] newtext; // will be aggressively reused!
1781 scope(exit) { delete newtext; newtext.length = 0; }
1783 int rereplace () {
1784 if (newtext.length) { newtext.length = 0; newtext.assumeSafeAppend; }
1785 auto reps = srr.replace;
1786 int spos = 0;
1787 mainloop: while (spos < reps.length) {
1788 if ((reps[spos] == '$' || reps[spos] == '\\') && reps.length-spos > 1 && reps[spos+1].isdigit) {
1789 int n = reps[spos+1]-'0';
1790 spos += 2;
1791 if (!caps[n].empty) foreach (char ch; this[caps[n].s..caps[n].e]) newtext ~= ch;
1792 } else if ((reps[spos] == '$' || reps[spos] == '\\') && reps.length-spos > 2 && reps[spos+1] == '{' && reps[spos+2].isdigit) {
1793 bool toupper, tolower, capitalize, uncapitalize;
1794 spos += 2;
1795 int n = 0;
1796 // parse number
1797 while (spos < reps.length && reps[spos].isdigit) n = n*10+reps[spos++]-'0';
1798 while (spos < reps.length && reps[spos] != '}') {
1799 switch (reps[spos++]) {
1800 case 'u': case 'U': toupper = true; tolower = false; break;
1801 case 'l': case 'L': tolower = true; toupper = false; break;
1802 case 'C': capitalize = true; break;
1803 case 'c': uncapitalize = true; break;
1804 default: // ignore other flags
1807 if (spos < reps.length && reps[spos] == '}') ++spos;
1808 if (n < caps.length && !caps[n].empty) {
1809 int tp = caps[n].s, ep = caps[n].e;
1810 char ch = gb[tp++];
1811 if (capitalize || toupper) ch = ch.toupper;
1812 else if (uncapitalize || tolower) ch = ch.tolower;
1813 newtext ~= ch;
1814 while (tp < ep) {
1815 ch = gb[tp++];
1816 if (toupper) ch = ch.toupper;
1817 else if (tolower) ch = ch.tolower;
1818 newtext ~= ch;
1821 } else if (reps[spos] == '\\' && reps.length-spos > 1) {
1822 spos += 2;
1823 switch (reps[spos-1]) {
1824 case 't': newtext ~= '\t'; break;
1825 case 'n': newtext ~= '\n'; break;
1826 case 'r': newtext ~= '\r'; break;
1827 case 'a': newtext ~= '\a'; break;
1828 case 'b': newtext ~= '\b'; break;
1829 case 'e': newtext ~= '\x1b'; break;
1830 case 'x': case 'X':
1831 if (reps.length-spos < 1) break mainloop;
1832 int n = digitInBase(reps[spos], 16);
1833 if (n < 0) break;
1834 ++spos;
1835 if (reps.length-spos > 0 && digitInBase(reps[spos], 16) >= 0) {
1836 n = n*16+digitInBase(reps[spos], 16);
1837 ++spos;
1839 newtext ~= cast(char)n;
1840 break;
1841 default:
1842 newtext ~= reps[spos-1];
1843 break;
1845 } else {
1846 newtext ~= reps[spos++];
1849 replaceText!"end"(caps[0].s, caps[0].e-caps[0].s, newtext);
1850 return cast(int)newtext.length;
1853 int count = 0;
1854 while (spos < epos) {
1855 bool found;
1856 if (srr.backwards) {
1857 found = findTextRegExpBack(re, spos, epos, caps[]);
1858 } else {
1859 found = findTextRegExp(re, spos, epos, caps[]);
1861 if (!found) break;
1862 // i found her!
1863 if (srr.nocomments && hl !is null) {
1864 auto lidx = lc.pos2line(caps[0].s);
1865 if (hl.fixLine(lidx)) markLinesDirty(lidx, 1); // so it won't lost dirty flag in redraw
1866 if (hiIsComment(gb.hi(caps[0].s))) {
1867 // skip
1868 if (!srr.backwards) spos = caps[0].s+1; else epos = caps[0].s;
1869 continue;
1872 if (!doAll) {
1873 bool doundo = (caps[0].s != curpos);
1874 if (doundo) gotoPos!true(caps[0].s);
1875 fullDirty();
1876 drawPage();
1877 drawPartHighlight(caps[0].s, caps[0].e-caps[0].s, IncSearchColor);
1878 auto act = dialogReplacePrompt(lc.pos2line(caps[0].s)-topline+winy);
1879 //FIXME: do undo only if it was succesfully registered
1880 //doUndo(); // undo cursor movement
1881 if (act == DialogRepPromptResult.Cancel) break;
1882 //if (doundo) doUndo();
1883 if (act == DialogRepPromptResult.Skip) {
1884 if (!srr.backwards) spos = caps[0].s+1; else epos = caps[0].s;
1885 continue;
1887 if (act == DialogRepPromptResult.All) { doAll = true; }
1889 if (doAll && !closeGroup) { undoGroupStart(); closeGroup = true; }
1890 int replen = rereplace();
1891 // fix search range
1892 if (!srr.backwards) {
1893 spos = caps[0].s+(replen ? replen : 1);
1894 epos += replen-(caps[0].e-caps[0].s);
1895 } else {
1896 epos = caps[0].s;
1898 if (doAll) ++count;
1900 if (doAll) {
1901 fullDirty();
1902 drawPage();
1903 dialogMessage!"info"("Search and Replace", "%s replacement%s made", count, (count != 1 ? "s" : ""));
1907 final:
1908 void processWordWith (scope char delegate (char ch) dg) {
1909 if (dg is null) return;
1910 bool undoAdded = false;
1911 scope(exit) if (undoAdded) undoGroupEnd();
1912 auto pos = curpos;
1913 while (pos < gb.textsize && gb[pos] <= ' ') ++pos;
1914 if (!isWordChar(gb[pos])) return;
1915 // find word start
1916 while (pos > 0 && isWordChar(gb[pos-1])) --pos;
1917 while (pos < gb.textsize) {
1918 auto ch = gb[pos];
1919 if (!isWordChar(gb[pos])) break;
1920 auto nc = dg(ch);
1921 if (ch != nc) {
1922 if (!undoAdded) { undoAdded = true; undoGroupStart(); }
1923 replaceText!"none"(pos, 1, (&nc)[0..1]);
1925 ++pos;
1927 gotoPos(pos);
1930 static bool isShitPPWordChar (char ch) {
1931 // '$' should come in too, but meh...
1932 return
1933 (ch >= 'A' && ch <= 'Z') ||
1934 (ch >= 'a' && ch <= 'z') ||
1935 (ch >= '0' && ch <= '9') ||
1936 ch == '_';
1939 // pos must be on ':', or this returns `false`
1940 bool isGoodDColonAt (int pos) {
1941 if (pos < 0 || pos >= gb.textsize) return false;
1942 if (gb[pos] != ':') return false;
1943 // if we are at 2nd (or more) colon, move to the first one
1944 if (pos > 0 && gb[pos-1] == ':') --pos;
1945 // now we should be at the first ':' in '::', check for valid position
1946 if (pos < 1 || pos+2 >= gb.textsize || gb[pos+1] != ':') return false; // not a '::', or invalid pos
1947 if (!isShitPPWordChar(gb[pos-1])) return false; // invalid boundaries
1948 if (!isShitPPWordChar(gb[pos+2]) && gb[pos+2] != '~') return false; // invalid boundaries (special '~' for dtors)
1949 return true;
1952 int vchGotoLineStart (int pos) {
1953 if (pos <= 0) return 0;
1954 while (pos > 0 && gb[pos-1] != '\n') --pos;
1955 return pos;
1958 int vchGotoLineEnd (int pos) {
1959 if (pos <= 0) pos = 0;
1960 while (pos < gb.textsize && gb[pos] != '\n') ++pos;
1961 return pos;
1964 int vchGotoPrevLine (int pos) {
1965 pos = vchGotoLineStart(pos);
1966 return vchGotoLineStart(pos-1);
1969 // skip current line
1970 int vchGotoSkipLine (int pos) {
1971 pos = vchGotoLineEnd(pos);
1972 // skip '\n'
1973 if (pos < gb.textsize) ++pos;
1974 return pos;
1977 // is empty single-line comment?
1978 // returns:
1979 // -1 for "not a comment"
1980 // 0 for "comment with data"
1981 // 1 for empty single-line comment
1982 int isLineAtPosASingleLineVCHComment (int pos) {
1983 pos = vchGotoLineStart(pos);
1984 // skip blanks
1985 while (pos < gb.textsize && gb[pos] != '\n' && gb[pos] <= ' ') ++pos;
1986 // check for single-line comment
1987 if (pos+2 > gb.textsize) return -1;
1988 if (gb[pos] != '/' || gb[pos+1] != '/') return -1;
1989 if (pos+2 == gb.textsize) return 1;
1990 if (gb[pos+2] == '/') return -1;
1991 pos += 2; // skip comment
1992 // check if we have any non-blank chars in it
1993 while (pos < gb.textsize && gb[pos] != '\n' && gb[pos] <= ' ') ++pos;
1994 return (pos >= gb.textsize || gb[pos] == '\n' ? 1 : 0);
1997 // good comment for Vavoom header?
1998 bool isLineAtPosAGoodVCHComment (int pos) {
1999 pos = vchGotoLineStart(pos);
2000 // skip blanks
2001 while (pos < gb.textsize && gb[pos] != '\n' && gb[pos] <= ' ') ++pos;
2002 // check for single-line comment
2003 if (pos+2 > gb.textsize) return false;
2004 if (gb[pos] != '/' || gb[pos+1] != '/') return false;
2005 if (pos+2 == gb.textsize) return true;
2006 if (gb[pos+2] == '/') return false;
2007 pos += 2; // skip comment
2008 // check if we have any non-blank chars in it
2009 while (pos < gb.textsize && gb[pos] != '\n' && gb[pos] <= ' ') ++pos;
2010 if (pos >= gb.textsize) return true; // dunno
2011 if (gb[pos] != '\n' || pos == 2) return true; // non-empty
2012 // for empty comments, allow only one single line (so check the previous line)
2013 // if previous line is "comment with data", allow this empty line
2014 pos = vchGotoPrevLine(pos);
2015 return (isLineAtPosASingleLineVCHComment(pos) == 0);
2018 // remove extra blanks (and insert required blanks) to good comment
2019 // WARNING! comment MUST be good
2020 // curpos must be at comment start, and will be on the next line
2021 void fixAGoodVCHCommentAndSkipIt () {
2022 auto pos = curpos;
2023 // delete leading spaces
2024 while (gb[pos] != '/') ++pos;
2025 if (pos > curpos) deleteText!"start"(curpos, pos-curpos);
2026 // skip '//'
2027 pos = curpos+2;
2028 // we should have exactly two spaces after '//'
2029 if (gb[pos] > ' ' || gb[pos] == '\n') {
2030 // insert two
2031 if (gb[pos] != '\n') insertText!("end", false)(pos, " ");
2032 } else if (gb[pos+1] > ' ' || gb[pos+1] == '\n') {
2033 // insert one
2034 if (gb[pos+1] != '\n') {
2035 insertText!("end", false)(pos+1, " ");
2036 } else {
2037 deleteText!"start"(pos, 1);
2039 } else if (gb[pos+2] <= ' ' && gb[pos+2] != '\n') {
2040 // have more remove extra
2041 pos += 2;
2042 int epos = pos+1;
2043 while (gb[epos] <= ' ' && gb[epos] != '\n') ++epos;
2044 deleteText!"start"(pos, epos-pos);
2046 // skip line
2047 pos = curpos;
2048 while (gb[pos] != '\n') ++pos;
2049 gotoPos(pos+1);
2052 // BUG: char at EOT is invalid
2053 bool vchCheckWord (int pos, string word) {
2054 if (word.length == 0 || pos >= gb.textsize) return false;
2055 if (isShitPPWordChar(gb[pos+cast(int)word.length])) return false;
2056 foreach (int f; 0..cast(int)word.length) if (gb[pos+f] != word[f]) return false;
2057 return true;
2060 // position cursor at single-line comment group
2061 // basically, just inserts "//***"
2062 void doGenVavoomRegionCmt () {
2063 auto pos = vchGotoLineStart(curpos);
2064 if (!isLineAtPosAGoodVCHComment(pos)) { ttyBeep(); return; } // invalid
2065 // find group start
2066 auto lastgoodpos = pos;
2067 while (pos > 0 && isLineAtPosAGoodVCHComment(pos)) {
2068 lastgoodpos = pos;
2069 pos = vchGotoLineStart(vchGotoPrevLine(pos));
2071 pos = lastgoodpos;
2072 bool insertSecond = (isLineAtPosASingleLineVCHComment(pos) == 0);
2073 // start undo group, so undo will remove the whole header at once
2074 undoGroupStart();
2075 scope(exit) undoGroupEnd();
2076 // create cutline
2077 char[] cutline;
2078 scope(exit) delete cutline;
2079 cutline.reserve(80);
2080 cutline ~= "//";
2081 while (cutline.length < 76) cutline ~= '*';
2082 cutline ~= '\n';
2083 // insert starting cutline
2084 gotoPos(pos);
2085 doPutText(cutline);
2086 // insert second line
2087 if (insertSecond) doPutText("//\n");
2088 // go to the end, and insert closing cutlines
2089 pos = curpos;
2090 bool prevWasEmpty = true;
2091 while (pos < gb.textsize && isLineAtPosAGoodVCHComment(pos)) {
2092 prevWasEmpty = (isLineAtPosASingleLineVCHComment(pos) > 0);
2093 pos = vchGotoSkipLine(pos);
2095 gotoPos(pos);
2096 if (!prevWasEmpty) doPutText("//\n");
2097 doPutText(cutline);
2098 // leave cursor here
2101 // position cursor at function/method name
2102 // this converts single-line comments
2103 void doGenVavoomCmt () {
2104 auto pos = curpos;
2105 if (isLineAtPosAGoodVCHComment(pos)) { ttyBeep(); return; } // invalid
2106 // find identifier start (including '::' for shitpp)
2107 while (pos >= 0) {
2108 if (gb[pos] == ':') {
2109 if (!isGoodDColonAt(pos)) { ttyBeep(); return; } // invalid
2110 } else {
2111 if (!isShitPPWordChar(gb[pos])) {
2112 // special for dtors
2113 if (gb[pos] == '~') {
2114 if (pos < 3 || gb[pos-1] != ':' || gb[pos-2] != ':') break;
2115 pos -= 1; // skip to '::' start (the following `--` will do the second move)
2116 } else if (pos > 0 && gb[pos] == '.') {
2117 // pascal
2118 if (!isShitPPWordChar(gb[pos-1])) break;
2119 } else {
2120 break;
2124 --pos;
2126 ++pos;
2127 // find identifier end
2128 auto epos = curpos;
2129 while (epos < gb.textsize) {
2130 if (gb[epos] == ':') {
2131 if (!isGoodDColonAt(epos)) { ttyBeep(); return; } // invalid
2132 } else {
2133 if (!isShitPPWordChar(gb[epos])) {
2134 // special for dtors
2135 if (gb[epos] == '~') {
2136 if (epos < 3 || gb[epos-1] != ':' || gb[epos-2] != ':') break;
2137 epos += 1; // skip to '::' end (the following `++` will do the second move)
2138 } else if (epos+1 < gb.textsize && gb[epos] == '.') {
2139 // pascal
2140 if (!isShitPPWordChar(gb[epos+1])) break;
2141 } else {
2142 break;
2146 ++epos;
2148 if (epos == pos) { ttyBeep(); return; } // invalid
2149 // [pos..epos) is our identifier; copy it into temp array
2150 char[] id;
2151 scope(exit) delete id;
2152 id.reserve(epos-pos+127);
2153 foreach (auto f; pos..epos) id ~= gb[f];
2154 if (id == "virtual" || id == "override") { ttyBeep(); return; } // invalid
2155 // if this is `operator`, collect operator name too
2156 if (id == "operator" || id.endsWith("::operator")) {
2157 auto xxpos = epos;
2158 while (epos < gb.textsize && gb[epos] != '\n' && gb[epos] <= ' ') ++epos;
2159 // is this '()'?
2160 if (epos+1 <= gb.textsize && gb[epos] == '(' && gb[epos+1] == ')') {
2161 epos += 2;
2162 } else {
2163 // find '('
2164 while (epos < gb.textsize && gb[epos] != '\n' && gb[epos] != '(') ++epos;
2166 // remove trailing spaces
2167 while (epos > xxpos && gb[epos-1] <= ' ') --epos;
2168 // append collected chars
2169 while (xxpos < epos) id ~= gb[xxpos++];
2171 // start undo group, so undo will remove the whole header at once
2172 undoGroupStart();
2173 scope(exit) undoGroupEnd();
2174 // move to line start (and save xpos)
2175 while (pos > 0 && gb[pos-1] != '\n') --pos;
2176 int xpos = curpos-pos;
2177 gotoPos(pos);
2178 // create cutline
2179 char[] cutline;
2180 scope(exit) delete cutline;
2181 cutline.reserve(80);
2182 cutline ~= "//";
2183 while (cutline.length < 76) cutline ~= '=';
2184 cutline ~= '\n';
2185 // skip good comments that we will add to header
2186 while (curpos > 0 && isLineAtPosAGoodVCHComment(vchGotoPrevLine(curpos))) {
2187 gotoPos(vchGotoPrevLine(curpos));
2189 // insert cutline (no indent)
2190 doPutText(cutline);
2191 // insert second line
2192 doPutText("//\n");
2193 // build third line
2194 doPutText("// ");
2195 doPutText(id);
2196 // finish third line, insert fourth line
2197 doPutText("\n//\n");
2198 // skip and normalize good comments
2199 if (isLineAtPosAGoodVCHComment(curpos)) {
2200 while (isLineAtPosAGoodVCHComment(curpos)) fixAGoodVCHCommentAndSkipIt();
2201 // and insert "nice line"
2202 doPutText("//\n");
2204 // insert final line
2205 doPutText(cutline);
2206 // back to identifier, so we can pretend that no cursor movement happened
2207 gotoPos(curpos+xpos);
2208 // now do some cosmetic: remove leading spaces, remove `virtual`
2209 pos = vchGotoLineStart(curpos);
2210 epos = pos;
2211 // it is safe to assume that we have non-blank chars here
2212 while (epos < gb.textsize && gb[epos] <= ' ') ++epos;
2213 // check if we have "virtual"
2214 if (vchCheckWord(epos, "virtual")) {
2215 // skip it
2216 epos += 7;
2217 // it is safe to assume that we have non-blank chars here
2218 while (epos < gb.textsize && gb[epos] <= ' ') ++epos;
2220 if (epos > pos) {
2221 gotoPos(curpos-(epos-pos));
2222 deleteText!"none"(pos, epos-pos);
2224 // delete "override" at the end
2225 int stpos = vchGotoLineStart(curpos);
2226 int endpos = vchGotoLineEnd(curpos);
2227 // they're guaranteed to be different
2228 if (gb[endpos-1] == '{' || gb[endpos-1] == ';') --endpos; /* this is how i usually doing it */
2229 const int realend = endpos;
2230 /* skip trailing spaces */
2231 while (endpos > stpos && gb[endpos-1] <= ' ') --endpos;
2232 /* check for the "override" word */
2233 immutable string ovword = "override";
2234 usize wpos = ovword.length;
2235 while (wpos > 0 && endpos > stpos && gb[endpos-1] == ovword[wpos-1]) {
2236 --wpos;
2237 --endpos;
2239 import std.ascii : isAlphaNum;
2240 if (wpos == 0 && endpos > stpos && !isAlphaNum(gb[endpos-1])) {
2241 /* delete it */
2242 deleteText!"none"(endpos, realend-endpos);
2246 void doGenCmtTearLine () {
2247 auto pos = curpos;
2248 // start undo group, so undo will remove the whole header at once
2249 undoGroupStart();
2250 scope(exit) undoGroupEnd();
2251 // move to line start (and save xpos)
2252 while (pos > 0 && gb[pos-1] != '\n') --pos;
2253 int xpos = curpos-pos;
2254 gotoPos(pos);
2255 // create tearline
2256 char[] tearline;
2257 scope(exit) delete tearline;
2258 tearline.reserve(90);
2259 while (tearline.length < 80) tearline ~= '/';
2260 tearline[2] = ' ';
2261 tearline[$-3] = ' ';
2262 tearline ~= '\n';
2263 // insert tearline
2264 doPutText(tearline);
2265 // back to identifier, so we can pretend that no cursor movement happened
2266 gotoPos(curpos+xpos);
2269 int doJumpBracketLastDir = 0; // for iterations
2271 void doJumpBracket () {
2272 if (repeatIteration == 0) doJumpBracketLastDir = 0;
2273 int curdir;
2274 auto pos = curpos;
2275 int newpos = -1;
2276 while (pos >= 0 && pos < gb.textsize) {
2277 newpos = -1;
2278 //if (!isAnyTextChar(pos, false)) return;
2279 auto ch = gb[pos];
2280 if (ch == '(') { newpos = drawHiBracket!false(pos, cy, ch, ')', 1); curdir = 1; }
2281 else if (ch == '{') { newpos = drawHiBracket!false(pos, cy, ch, '}', 1, true); curdir = 1; }
2282 else if (ch == '[') { newpos = drawHiBracket!false(pos, cy, ch, ']', 1); curdir = 1; }
2283 else if (ch == ')') { newpos = drawHiBracket!false(pos, cy, ch, '(', -1); curdir = -1; }
2284 else if (ch == '}') { newpos = drawHiBracket!false(pos, cy, ch, '{', -1, true); curdir = -1; }
2285 else if (ch == ']') { newpos = drawHiBracket!false(pos, cy, ch, '[', -1); curdir = -1; }
2286 else {
2287 if (doJumpBracketLastDir == 0) return;
2288 pos += doJumpBracketLastDir;
2289 continue;
2291 if (doJumpBracketLastDir != 0 && curdir == doJumpBracketLastDir) break;
2292 if (doJumpBracketLastDir == 0) { doJumpBracketLastDir = curdir; break; }
2293 pos += doJumpBracketLastDir;
2295 if (newpos >= 0) gotoPos(newpos);
2299 final:
2300 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("Up", q{ doUp(); });
2301 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("S-Up", q{ doUp(true); });
2302 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("C-Up", q{ doScrollUp(); });
2303 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("S-C-Up", q{ doScrollUp(true); });
2304 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("Down", q{ doDown(); });
2305 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("S-Down", q{ doDown(true); });
2306 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("C-Down", q{ doScrollDown(); });
2307 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("S-C-Down", q{ doScrollDown(true); });
2309 @TEDRepeated mixin TEDImpl!("C-]", q{ doJumpBracket(); });
2311 @TEDRepeated mixin TEDImpl!("Left", q{ doLeft(); });
2312 @TEDRepeated mixin TEDImpl!("S-Left", q{ doLeft(true); });
2314 @TEDRepeated mixin TEDImpl!("C-Left", q{ doWordLeft(); });
2315 @TEDRepeated mixin TEDImpl!("S-C-Left", q{ doWordLeft(true); });
2316 @TEDRepeated mixin TEDImpl!("Right", q{ doRight(); });
2317 @TEDRepeated mixin TEDImpl!("S-Right", q{ doRight(true); });
2318 @TEDRepeated mixin TEDImpl!("C-Right", q{ doWordRight(); });
2319 @TEDRepeated mixin TEDImpl!("S-C-Right", q{ doWordRight(true); });
2321 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("PageUp", q{ doPageUp(); });
2322 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("S-PageUp", q{ doPageUp(true); });
2323 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("C-PageUp", q{ doTextTop(); });
2324 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("S-C-PageUp", q{ doTextTop(true); });
2325 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("PageDown", q{ doPageDown(); });
2326 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("S-PageDown", q{ doPageDown(true); });
2327 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("C-PageDown", q{ doTextBottom(); });
2328 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("S-C-PageDown", q{ doTextBottom(true); });
2329 @TEDRepeated mixin TEDImpl!("Home", q{ doHome(); });
2330 @TEDRepeated mixin TEDImpl!("S-Home", q{ doHome(true, true); });
2331 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("C-Home", q{ doPageTop(); });
2332 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("S-C-Home", q{ doPageTop(true); });
2333 @TEDRepeated mixin TEDImpl!("End", q{ doEnd(); });
2334 @TEDRepeated mixin TEDImpl!("S-End", q{ doEnd(true); });
2335 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("C-End", q{ doPageBottom(); });
2336 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("S-C-End", q{ doPageBottom(true); });
2338 @TEDEditOnly @TEDRepeated mixin TEDImpl!("Backspace", q{ doBackspace(); });
2339 @TEDSingleOnly @TEDEditOnly @TEDRepeated mixin TEDImpl!("M-Backspace", "delete previous word", q{ doDeleteWord(); });
2340 @TEDMultiOnly @TEDEditOnly @TEDRepeated mixin TEDImpl!("M-Backspace", "delete previous word or unindent", q{ doBackByIndent(); });
2342 @TEDRepeated mixin TEDImpl!("Delete", q{ doDelete(); });
2343 mixin TEDImpl!("C-Insert", "copy block to clipboard file, reset block mark", q{ if (tempBlockFileName.length == 0) return; doBlockWrite(tempBlockFileName); doBlockResetMark(); });
2345 @TEDMultiOnly @TEDEditOnly @TEDRepeated mixin TEDImpl!("Enter", q{ doPutChar('\n'); });
2346 @TEDMultiOnly @TEDEditOnly @TEDRepeated mixin TEDImpl!("M-Enter", "split line without autoindenting", q{ doLineSplit(false); });
2349 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("F2", "save file", q{ saveFile(fullFileName); });
2350 mixin TEDImpl!("F3", "start/stop/reset block marking", q{ doToggleBlockMarkMode(); });
2351 mixin TEDImpl!("C-F3", "reset block mark", q{ doBlockResetMark(); });
2352 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("F4", "search and relace text", q{
2353 srrOptions.inselenabled = hasMarkedBlock;
2354 srrOptions.utfuck = utfuck;
2355 if (!dialogSearchReplace(hisman, srrOptions)) return;
2356 if (srrOptions.type == SearchReplaceOptions.Type.Normal) { srrPlain(srrOptions); return; }
2357 if (srrOptions.type == SearchReplaceOptions.Type.Regex) { srrRegExp(srrOptions); return; }
2358 dialogMessage!"error"("Not Yet", "This S&R type is not supported yet!");
2360 @TEDEditOnly mixin TEDImpl!("F5", "copy block", q{ doBlockCopy(); });
2361 mixin TEDImpl!("C-F5", "copy block to clipboard file", q{ if (tempBlockFileName.length == 0) return; doBlockWrite(tempBlockFileName); });
2362 @TEDEditOnly mixin TEDImpl!("S-F5", "insert block from clipboard file", q{ if (tempBlockFileName.length == 0) return; waitingInF5 = true; });
2363 @TEDEditOnly mixin TEDImpl!("F6", "move block", q{ doBlockMove(); });
2364 @TEDEditOnly mixin TEDImpl!("F8", "delete block", q{ doBlockDelete(); });
2366 mixin TEDImpl!("C-A", "move to line start", q{ doHome(); });
2367 mixin TEDImpl!("C-E", "move to line end", q{ doEnd(); });
2369 @TEDMultiOnly mixin TEDImpl!("M-I", "jump to previous bookmark", q{ doBookmarkJumpUp(); });
2370 @TEDMultiOnly mixin TEDImpl!("M-J", "jump to next bookmark", q{ doBookmarkJumpDown(); });
2371 @TEDMultiOnly mixin TEDImpl!("M-K", "toggle bookmark", q{ doBookmarkToggle(); });
2372 @TEDMultiOnly mixin TEDImpl!("M-L", "goto line", q{
2373 auto lnum = dialogLineNumber(hisman);
2374 if (lnum > 0 && lnum < linecount) gotoXY!true(curx, lnum-1); // vcenter
2377 @TEDEditOnly @TEDRepeated mixin TEDImpl!("M-C", "capitalize word", q{
2378 bool first = true;
2379 processWordWith((char ch) {
2380 if (first) { first = false; ch = ch.toupper; }
2381 return ch;
2384 @TEDEditOnly @TEDRepeated mixin TEDImpl!("M-Q", "lowercase word", q{ processWordWith((char ch) => ch.tolower); });
2385 @TEDEditOnly @TEDRepeated mixin TEDImpl!("M-U", "uppercase word", q{ processWordWith((char ch) => ch.toupper); });
2387 @TEDMultiOnly mixin TEDImpl!("M-S-L", "force center current line", q{ makeCurLineVisibleCentered(true); });
2388 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("C-R", "continue incremental search, forward", q{ incSearchDir = 1; if (incSearchBuf.length == 0 && !incInputActive) doStartIncSearch(1); else doNextIncSearch(); });
2389 @TEDEditOnly @TEDRepeated mixin TEDImpl!("C-U", "undo", q{ doUndo(); });
2390 @TEDEditOnly @TEDRepeated mixin TEDImpl!("M-U", "undo", q{ doUndo(); });
2391 @TEDEditOnly @TEDRepeated mixin TEDImpl!("M-S-U", "redo", q{ doRedo(); });
2392 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("C-V", "continue incremental search, backward", q{ incSearchDir = -1; if (incSearchBuf.length == 0 && !incInputActive) doStartIncSearch(-1); else doNextIncSearch(); });
2393 @TEDEditOnly @TEDRepeated mixin TEDImpl!("C-W", "remove previous word", q{ doDeleteWord(); });
2394 @TEDEditOnly @TEDRepeated mixin TEDImpl!("C-Y", "remove current line", q{ doKillLine(); });
2395 @TEDMultiOnly mixin TEDImpl!("C-_", "start new incremental search, forward", q{ doStartIncSearch(1); }); // ctrl+slash, actually
2396 @TEDMultiOnly mixin TEDImpl!("C-\\", "start new incremental search, backward", q{ doStartIncSearch(-1); });
2398 @TEDMultiOnly @TEDEditOnly @TEDRepeated mixin TEDImpl!("Tab", q{ doPutText(" "); });
2399 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("M-Tab", "autocomplete word", q{ doAutoComplete(); });
2400 @TEDMultiOnly @TEDEditOnly @TEDRepeated mixin TEDImpl!("C-Tab", "indent block", q{ doIndentBlock(); });
2401 @TEDMultiOnly @TEDEditOnly @TEDRepeated mixin TEDImpl!("C-S-Tab", "unindent block", q{ doUnindentBlock(); });
2403 mixin TEDImpl!("M-S-c", "copy block to X11 selections (all three)", q{ pasteToX11(); doBlockResetMark(); });
2405 @TEDMultiOnly mixin TEDImpl!("M-E", "select codepage", q{
2406 auto ncp = dialogCodePage(utfuck ? 3 : codepage);
2407 if (ncp < 0) return;
2408 if (ncp > CodePage.max) { utfuck = true; return; }
2409 utfuck = false;
2410 codepage = cast(CodePage)ncp;
2411 fullDirty();
2414 @TEDMultiOnly @TEDEditOnly @TEDRepeated mixin TEDImpl!("C-K C-I", "indent block", q{ doIndentBlock(); });
2415 @TEDMultiOnly @TEDEditOnly @TEDRepeated mixin TEDImpl!("C-K C-U", "unindent block", q{ doUnindentBlock(); });
2416 @TEDEditOnly mixin TEDImpl!("C-K C-E", "clear from cursor to EOL", q{ doKillToEOL(); });
2417 @TEDMultiOnly @TEDEditOnly @TEDRepeated mixin TEDImpl!("C-K Tab", "indent block", q{ doIndentBlock(); });
2418 @TEDEditOnly @TEDRepeated mixin TEDImpl!("C-K M-Tab", "untabify", q{ doUntabify(lc.tabsize ? lc.tabsize : 2); }); // alt+tab: untabify
2419 @TEDEditOnly mixin TEDImpl!("C-K C-space", "remove trailing spaces", q{ doRemoveTailingSpaces(); });
2420 mixin TEDImpl!("C-K C-T", /*"toggle \"visual tabs\" mode",*/ q{ visualtabs = !visualtabs; });
2422 @TEDMultiOnly mixin TEDImpl!("C-K C-B", q{ doSetBlockStart(); });
2423 @TEDMultiOnly mixin TEDImpl!("C-K C-K", q{ doSetBlockEnd(); });
2425 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-C", q{ doBlockCopy(); });
2426 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-M", q{ doBlockMove(); });
2427 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-Y", q{ doBlockDelete(); });
2428 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-H", q{ doBlockResetMark(); });
2429 // fuckin' vt100!
2430 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K Backspace", q{ doBlockResetMark(); });
2432 mixin TEDImpl!("C-K !", "toggle read-only mode", q{ ttyBeep(); auto ro = readonly; readonly = !ro; fullDirty(); });
2434 @TEDEditOnly @TEDRepeated mixin TEDImpl!("C-Q Tab", q{ doPutChar('\t'); });
2435 mixin TEDImpl!("C-Q C-U", "toggle utfuck mode", q{ utfuck = !utfuck; }); // ^Q^U: switch utfuck mode
2436 mixin TEDImpl!("C-Q 1", "switch to koi8", q{ utfuck = false; codepage = CodePage.koi8u; fullDirty(); });
2437 mixin TEDImpl!("C-Q 2", "switch to cp1251", q{ utfuck = false; codepage = CodePage.cp1251; fullDirty(); });
2438 mixin TEDImpl!("C-Q 3", "switch to cp866", q{ utfuck = false; codepage = CodePage.cp866; fullDirty(); });
2439 mixin TEDImpl!("C-Q C-B", "go to block start", q{ if (hasMarkedBlock) gotoPos!true(bstart); lastBGEnd = false; });
2441 @TEDMultiOnly @TEDRepeated mixin TEDImpl!("C-Q C-F", "incremental search current word", q{
2442 auto pos = curpos;
2443 if (!isWordChar(gb[pos])) return;
2444 // deactivate prompt
2445 if (incInputActive) {
2446 incInputActive = false;
2447 promptDeactivate();
2448 resetIncSearchPos();
2450 // collect word
2451 while (pos > 0 && isWordChar(gb[pos-1])) --pos;
2452 incSearchBuf.length = 0;
2453 incSearchBuf.assumeSafeAppend;
2454 while (pos < gb.textsize && isWordChar(gb[pos])) incSearchBuf ~= gb[pos++];
2455 incSearchDir = 1;
2456 // get current word
2457 doNextIncSearch();
2460 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-Q C-H", "generate Vavoom function header comment", q{ doGenVavoomCmt(); });
2461 // hack for idiotic terminal mapping
2462 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-Q Backspace", "generate Vavoom function header comment", q{ doGenVavoomCmt(); });
2464 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-Q C-X C-H", "generate Vavoom region header comment", q{ doGenVavoomRegionCmt(); });
2465 // hack for idiotic terminal mapping
2466 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-Q C-X Backspace", "generate Vavoom region header comment", q{ doGenVavoomRegionCmt(); });
2468 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-Q C-M", "put comment tearline", q{ doGenCmtTearLine(); });
2469 // hack for idiotic terminal mapping
2470 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-Q Enter", "put comment tearline", q{ doGenCmtTearLine(); });
2472 mixin TEDImpl!("C-Q C-K", "go to block end", q{ if (hasMarkedBlock) gotoPos!true(bend); lastBGEnd = true; });
2473 mixin TEDImpl!("C-Q C-T", "set tab size", q{
2474 auto tsz = dialogTabSize(hisman, tabsize);
2475 if (tsz > 0 && tsz <= 64) tabsize = cast(ubyte)tsz;
2478 @TEDMultiOnly mixin TEDImpl!("C-Q C-X C-S", "toggle syntax highlighing", q{ toggleHighlighting(); });
2480 @TEDMultiOnly @TEDROOnly @TEDRepeated mixin TEDImpl!("Space", q{ doPageDown(); });
2481 @TEDMultiOnly @TEDROOnly @TEDRepeated mixin TEDImpl!("C-Space", q{ doPageUp(); });
2483 @TEDRepeatChar mixin TEDImpl!("C-P", "counter (*2 or direct number)", q{ doCounterMode(); });
2484 @TEDRepeatChar mixin TEDImpl!("C-]", "", q{ doCounterMode(); });