egeditor: some highlighting fixes
[iv.d.git] / tuing / ttyeditor.d
blob0b0d0f0ea66be2e5b39430a9799f7955f7cccf47
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.tuing.ttyeditor /*is aliced*/;
18 import std.datetime : SysTime;
20 import iv.alice;
21 import iv.eventbus;
22 import iv.flexlayout;
23 import iv.rawtty;
24 import iv.srex;
25 import iv.strex;
26 import iv.utfutil;
27 import iv.vfs.io;
29 import iv.egeditor.editor;
30 import iv.egeditor.highlighters;
32 import iv.tuing.types;
33 import iv.tuing.tty;
34 import iv.tuing.events;
35 import iv.tuing.tui;
38 // ////////////////////////////////////////////////////////////////////////// //
39 private string normalizedAbsolutePath (string path) {
40 import std.path;
41 return buildNormalizedPath(absolutePath(path));
45 // ////////////////////////////////////////////////////////////////////////// //
46 class TtyEditor : EditorEngine {
47 static struct SROptions {
48 TtyEditor ed;
49 enum Type : int {
50 Normal,
51 Regex,
53 const(char)[] search;
54 const(char)[] replace;
55 Type type;
56 bool casesens;
57 bool backwards;
58 bool wholeword;
59 bool inselection;
60 bool nocomments;
61 // the following fields are relevant for "replacement continuation"
62 int spos, epos;
63 int mts, mte; // match start and end
64 int repcount;
65 enum Cont {
66 Cancel = -1,
67 No = 0,
68 Yes = 1,
69 All = 2,
71 Cont cont;
72 bool closeGroup = false; // close undo group?
73 Pike.Capture[64] caps;
74 RegExp re;
75 char[] newtext;
78 static {
79 // ////////////////////////////////////////////////////////////////////////// //
80 public enum TextBG = TtyRgb2Color!(0x3a, 0x3a, 0x3a); // 237
82 public enum TextColor = XtColorFB!(TtyRgb2Color!(0xd0, 0xd0, 0xd0), TextBG); // 252,237
83 public enum TextKillColor = XtColorFB!(TtyRgb2Color!(0xe0, 0xe0, 0xe0), TextBG); // 252,237
84 public enum BadColor = XtColorFB!(TtyRgb2Color!(0xff, 0xff, 0x54), TtyRgb2Color!(0xb2, 0x18, 0x18)); // 11,1
85 //public enum TrailSpaceColor = XtColorFB!(TtyRgb2Color!(0xff, 0xff, 0x00), TtyRgb2Color!(0x00, 0x00, 0x87)); // 226,18
86 public enum TrailSpaceColor = XtColorFB!(TtyRgb2Color!(0x6c, 0x6c, 0x6c), TtyRgb2Color!(0x26, 0x26, 0x26)); // 242,235
87 public enum BlockColor = XtColorFB!(TtyRgb2Color!(0xff, 0xff, 0xff), TtyRgb2Color!(0x00, 0x5f, 0xff)); // 15,27
88 public enum BookmarkColor = XtColorFB!(TtyRgb2Color!(0xff, 0xff, 0xff), TtyRgb2Color!(0x87, 0x00, 0xd7)); // 15,92
89 public enum BracketColor = XtColorFB!(TtyRgb2Color!(0xff, 0xff, 0x54), TtyRgb2Color!(0x00, 0x00, 0x00)); // 11,0
90 public enum IncSearchColor = XtColorFB!(TtyRgb2Color!(0xff, 0xff, 0x00), TtyRgb2Color!(0xd7, 0x00, 0x00)); // 226,160
92 public enum UtfuckedColor = XtColorFB!(TtyRgb2Color!(0x6c, 0x6c, 0x6c), TtyRgb2Color!(0x26, 0x26, 0x26)); // 242,235
94 public enum VLineColor = XtColorFB!(TtyRgb2Color!(0x60, 0x60, 0x60), TextBG); // 252,237
96 //public enum TabColor = XtColorFB!(TtyRgb2Color!(0x00, 0x00, 0x80), TextBG);
99 // ////////////////////////////////////////////////////////////////////////// //
100 public uint hiColor() (in auto ref GapBuffer.HighState hs) nothrow @safe @nogc {
101 switch (hs.kwtype) {
102 case HiNone: return XtColorFB!(TtyRgb2Color!(0xb2, 0xb2, 0xb2), TtyRgb2Color!(0x00, 0x00, 0x00)); // 7,0
103 case HiText: return TextColor;
105 case HiCommentOneLine:
106 case HiCommentMulti:
107 return XtColorFB!(TtyRgb2Color!(0xb2, 0x68, 0x18), TextBG); // 3,237
109 case HiNumber:
110 return XtColorFB!(TtyRgb2Color!(0x18, 0xb2, 0x18), TextBG); // 2,237
112 case HiChar:
113 return XtColorFB!(TtyRgb2Color!(0x54, 0xff, 0xff), TextBG); // 14,237
114 case HiCharSpecial:
115 return XtColorFB!(TtyRgb2Color!(0x54, 0xff, 0x54), TextBG); // 10,237; green
116 //return XtColorFB!(TtyRgb2Color!(0x18, 0xb2, 0x18), TextBG); // 2,237
117 //return XtColorFB!(TtyRgb2Color!(0x54, 0xff, 0xff), TextBG); // 14,237
119 // normal string
120 case HiString:
121 case HiSQString:
122 case HiBQString:
123 case HiRQString:
124 return XtColorFB!(TtyRgb2Color!(0x18, 0xb2, 0xb2), TextBG); // 6,237
125 case HiStringSpecial:
126 case HiSQStringSpecial:
127 return XtColorFB!(TtyRgb2Color!(0x54, 0xff, 0xff), TextBG); // 14,237
128 //return XtColorFB!(TtyRgb2Color!(0x18, 0xb2, 0x18), TextBG); // 2,237
130 case HiKeyword: return XtColorFB!(TtyRgb2Color!(0xff, 0xff, 0x54), TextBG); // 11,237
131 case HiKeywordHi: return XtColorFB!(TtyRgb2Color!(0xff, 0xff, 0xff), TextBG); // 202,237
132 case HiBuiltin: return XtColorFB!(TtyRgb2Color!(0xff, 0x5f, 0x00), TextBG); // 202,237
133 case HiType: return XtColorFB!(TtyRgb2Color!(0xff, 0xaf, 0x00), TextBG); // 214,237
134 case HiSpecial: return XtColorFB!(TtyRgb2Color!(0x54, 0xff, 0x54), TextBG); // 10,237; green
135 case HiInternal: return XtColorFB!(TtyRgb2Color!(0xff, 0x54, 0x54), TextBG); // 9,237; red
136 case HiPunct: return XtColorFB!(TtyRgb2Color!(0x54, 0xff, 0xff), TextBG); // 14,237
137 case HiSemi: return XtColorFB!(TtyRgb2Color!(0xff, 0x00, 0xff), TextBG); // 201,237
138 case HiUDA: return XtColorFB!(TtyRgb2Color!(0x00, 0x87, 0xff), TextBG); // 33,237
139 case HiAliced: return XtColorFB!(TtyRgb2Color!(0xff, 0x5f, 0x00), TextBG); // 202,237
140 case HiPreprocessor: return XtColorFB!(TtyRgb2Color!(0xff, 0x54, 0x54), TextBG); // 9,237; red
142 case HiRegExp: return XtColorFB!(TtyRgb2Color!(0xff, 0x5f, 0x00), TextBG); // 202,237
144 case HiToDoOpen: // [.]
145 return XtColorFB!(TtyRgb2Color!(0xff, 0x00, 0xff), TextBG);
146 case HiToDoUnsure: // [?]
147 return XtColorFB!(TtyRgb2Color!(0xc0, 0x00, 0xc0), TextBG);
148 case HiToDoUrgent: // [!]
149 return XtColorFB!(TtyRgb2Color!(0xff, 0x00, 0x00), TextBG);
150 case HiToDoSemi: // [+]
151 return XtColorFB!(TtyRgb2Color!(0xff, 0xff, 0x00), TextBG);
152 case HiToDoDone: // [*]
153 return XtColorFB!(TtyRgb2Color!(0x00, 0xa0, 0x00), TextBG);
154 case HiToDoDont: // [-]
155 return XtColorFB!(TtyRgb2Color!(0x90, 0x90, 0x00), TextBG);
157 default: assert(0, "wtf?!");
161 // ////////////////////////////////////////////////////////////////////////// //
162 // new higlighter instance for the file with the given extension
163 public __gshared EditorHL getHiglighterFor (const(char)[] ext, const(char)[] fullname) {
164 if (ext.strEquCI(".d")) {
165 __gshared EdHiTokensD toksd;
166 if (toksd is null) toksd = new EdHiTokensD();
167 return new EditorHLExt(toksd);
169 if (ext.strEquCI(".js") || ext.strEquCI(".jsm")) {
170 __gshared EdHiTokensJS toksjs;
171 if (toksjs is null) toksjs = new EdHiTokensJS();
172 return new EditorHLExt(toksjs);
174 if (ext.strEquCI(".c") || ext.strEquCI(".cpp") || ext.strEquCI(".h") || ext.strEquCI(".hpp")) {
175 __gshared EdHiTokensC toksc;
176 if (toksc is null) toksc = new EdHiTokensC();
177 return new EditorHLExt(toksc);
179 if (ext.strEquCI(".sh") || ext.strEquCI(".profile")) {
180 __gshared EdHiTokensShell tokssh;
181 if (tokssh is null) tokssh = new EdHiTokensShell();
182 return new EditorHLExt(tokssh);
184 auto bnpos = fullname.length;
185 while (bnpos > 0 && fullname.ptr[bnpos-1] != '/') --bnpos;
186 auto name = fullname[bnpos..$];
187 if (name == "TODO") return new EditorHLTODO();
188 if (name == "COMMIT_EDITMSG") return new EditorHLGitCommit();
189 return null;
193 enum TEDSingleOnly; // only for single-line mode
194 enum TEDMultiOnly; // only for multiline mode
195 enum TEDEditOnly; // only for non-readonly mode
196 enum TEDROOnly; // only for readonly mode
198 static struct TEDKey { string key; string help; } // UDA
200 static string TEDImplX(string key, string help, string code, usize ln) () {
201 static assert(key.length > 0, "wtf?!");
202 static assert(code.length > 0, "wtf?!");
203 string res = "@TEDKey("~key.stringof~", "~help.stringof~") void _ted_";
204 int pos = 0;
205 while (pos < key.length) {
206 char ch = key[pos++];
207 if (key.length-pos > 0 && key[pos] == '-') {
208 if (ch == 'C' || ch == 'c') { ++pos; res ~= "Ctrl"; continue; }
209 if (ch == 'M' || ch == 'm') { ++pos; res ~= "Alt"; continue; }
210 if (ch == 'S' || ch == 's') { ++pos; res ~= "Shift"; continue; }
212 if (ch == '^') { res ~= "Ctrl"; continue; }
213 if (ch >= 'a' && ch <= 'z') ch -= 32;
214 if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || ch == '_') res ~= ch; else res ~= '_';
216 res ~= ln.stringof;
217 res ~= " () {"~code~"}";
218 return res;
221 mixin template TEDImpl(string key, string help, string code, usize ln=__LINE__) {
222 mixin(TEDImplX!(key, help, code, ln));
225 mixin template TEDImpl(string key, string code, usize ln=__LINE__) {
226 mixin(TEDImplX!(key, "", code, ln));
229 protected:
230 static struct TextHighlight {
231 int pos, count;
232 uint clr;
233 @property bool valid () const pure nothrow @safe @nogc { return (count > 0); }
235 TextHighlight texthi;
237 TtyEvent[32] comboBuf;
238 int comboCount; // number of items in `comboBuf`
239 bool waitingInF5;
240 bool incInputActive;
242 protected:
243 TtyEditor mPromptInput; // input line for a prompt; lazy creation
244 bool mPromptActive;
245 char[128] mPromptPrompt; // lol
246 int mPromptLen;
248 final void promptDeactivate () {
249 if (mPromptActive) {
250 mPromptActive = false;
251 fullDirty(); // just in case
255 final void promptNoKillText () {
256 if (mPromptInput !is null) mPromptInput.killTextOnChar = false;
259 final void promptActivate (const(char)[] prompt=null, const(char)[] text=null) {
260 if (mPromptInput is null) {
261 mPromptInput = new TtyEditor(0, 0, 10, 1, true); // single-lined
264 bool addDotDotDot = false;
265 if (winw <= 8) {
266 prompt = null;
267 } else {
268 if (prompt.length > winw-8) {
269 addDotDotDot = true;
270 prompt = prompt[$-(winw-8)..$];
273 if (prompt.length > mPromptPrompt.length) addDotDotDot = true;
275 mPromptPrompt[] = 0;
276 if (addDotDotDot) {
277 mPromptPrompt[0..3] = '.';
278 if (prompt.length > mPromptPrompt.length-3) prompt = prompt[$-(mPromptPrompt.length-3)..$];
279 mPromptPrompt[3..3+prompt.length] = prompt[];
280 mPromptLen = cast(int)prompt.length+3;
281 } else {
282 mPromptPrompt[0..prompt.length] = prompt[];
283 mPromptLen = cast(int)prompt.length;
286 mPromptInput.moveResize(winx+mPromptLen+2, winy-1, winw-mPromptLen-2, 1);
287 mPromptInput.clear();
288 if (text.length) {
289 mPromptInput.doPutText(text);
290 mPromptInput.clearUndo();
293 mPromptInput.killTextOnChar = true;
294 mPromptInput.clrBlock = XtColorFB!(TtyRgb2Color!(0xff, 0xff, 0xff), TtyRgb2Color!(0x00, 0x5f, 0xff));
295 mPromptInput.clrText = XtColorFB!(TtyRgb2Color!(0x00, 0x00, 0x00), TtyRgb2Color!(0xff, 0x7f, 0x00));
296 mPromptInput.clrTextUnchanged = XtColorFB!(TtyRgb2Color!(0x00, 0x00, 0x00), TtyRgb2Color!(0xcf, 0x4f, 0x00));
298 mPromptInput.utfuck = utfuck;
299 mPromptInput.codepage = codepage;
300 mPromptActive = true;
301 fullDirty(); // just in case
304 final bool promptProcessKey (TtyEvent key, scope void delegate (EditorEngine ed) onChange=null) {
305 if (!mPromptActive) return false;
306 auto lastCC = mPromptInput.bufferCC;
307 auto res = mPromptInput.processKey(key);
308 if (lastCC != mPromptInput.bufferCC && onChange !is null) onChange(mPromptInput);
309 return res;
312 protected:
313 int incSearchDir; // -1; 0; 1
314 char[] incSearchBuf; // will be actively reused, don't expose
315 int incSearchHitPos = -1;
316 int incSearchHitLen = -1;
318 protected:
319 // autocompletion buffers
320 const(char)[][256] aclist; // autocompletion tokens
321 uint acused = 0; // number of found tokens
322 char[] acbuffer; // will be actively reused, don't expose
324 public:
325 string logFileName;
326 string tempBlockFileName;
327 string fullFileName;
328 // save this, so we can check on saving
329 SysTime fileModTime;
330 long fileDiskSize = -1; // <0: modtime is invalid
331 bool dontSetCursor; // true: don't gotoxy to cursor position
332 // colors; valid for singleline control
333 uint clrBlock, clrText, clrTextUnchanged;
334 // other
335 SROptions srrOptions;
336 bool hideStatus = false;
337 bool hideSBar = false; // hide scrollbar
338 bool editorlocked = false; // when editor send some message that needs action reply, it locks itself
340 public:
341 this (int x0, int y0, int w, int h, bool asinglesine=false) {
342 // prompt should be created only for multiline editors
343 super(x0, y0, w, h, null, asinglesine);
344 //srrOptions.type = SROptions.Type.Regex;
345 srrOptions.casesens = true;
346 addEventListener(this, (EventEditorReplyGotoLine evt) {
347 if (evt.line > 0 && evt.line <= linecount) gotoXY!true(curx, evt.line-1); // vcenter
349 addEventListener(this, (EventEditorReplyCodePage evt) {
350 auto ncp = evt.cp;
351 if (ncp < 0) return;
352 if (ncp > CodePage.max) { utfuck = true; return; }
353 utfuck = false;
354 codepage = cast(CodePage)ncp;
355 fullDirty();
356 //(new EventEditorMessage(this, "codepage set")).post;
358 addEventListener(this, (EventEditorReplyTabSize evt) {
359 auto tsz = evt.tabsize;
360 if (tsz > 0 && tsz <= 64) tabsize = cast(ubyte)tsz;
361 //import std.conv : to;
362 //(new EventEditorMessage(this, "tabsize set to "~tabsize.to!string)).post;
366 // call this after setting `fullFileName`
367 void setupHighlighter () {
368 auto ep = fullFileName.length;
369 while (ep > 0 && fullFileName.ptr[ep-1] != '/' && fullFileName.ptr[ep-1] != '.') --ep;
370 if (ep < 1 || fullFileName.ptr[ep-1] != '.') {
371 attachHiglighter(getHiglighterFor("", fullFileName));
372 } else {
373 attachHiglighter(getHiglighterFor(fullFileName[ep-1..$], fullFileName));
377 final void getDiskFileInfo () {
378 import std.file : getSize, timeLastModified;
379 if (fullFileName.length) {
380 fileDiskSize = getSize(fullFileName);
381 fileModTime = timeLastModified(fullFileName);
382 } else {
383 fileDiskSize = -1;
387 // return `true` if file was changed
388 final bool wasDiskFileChanged () {
389 import std.file : exists, getSize, timeLastModified;
390 if (fullFileName.length && fileDiskSize >= 0) {
391 if (!fullFileName.exists) return false;
392 auto sz = getSize(fullFileName);
393 if (sz != fileDiskSize) return true;
394 SysTime modtime = timeLastModified(fullFileName);
395 if (modtime != fileModTime) return true;
397 return false;
400 override void loadFile (const(char)[] fname) {
401 fileDiskSize = -1;
402 fullFileName = normalizedAbsolutePath(fname.idup);
403 super.loadFile(VFile(fullFileName));
404 getDiskFileInfo();
407 final void checkDiskAndReloadPrompt () {
408 if (fullFileName.length == 0) return;
409 if (wasDiskFileChanged) {
410 auto oldlk = editorlocked;
411 addEventListener(this, (EventEditorReplyReloadModified evt) {
412 editorlocked = oldlk;
413 if (evt.res > 0) {
414 int rx = cx, ry = cy;
415 clear();
416 super.loadFile(VFile(fullFileName));
417 getDiskFileInfo();
418 cx = rx;
419 cy = ry;
420 normXY();
421 makeCurLineVisibleCentered();
423 }, true); // oneshot
424 editorlocked = true;
425 (new EventEditorQueryReloadModified(this)).post;
429 final void forceSaveFile () {
430 if (fullFileName.length == 0) return;
431 super.saveFile(fullFileName);
432 getDiskFileInfo();
435 override void saveFile (const(char)[] fname=null) {
436 if (fname.length) {
437 auto nfn = normalizedAbsolutePath(fname.idup);
438 if (fname != fullFileName) {
439 super.saveFile(VFile(nfn, "w"));
440 fullFileName = nfn;
441 getDiskFileInfo();
442 return;
445 if (fullFileName.length == 0) return;
446 if (wasDiskFileChanged) {
447 auto oldlk = editorlocked;
448 addEventListener(this, (EventEditorReplyOverwriteModified evt) {
449 editorlocked = oldlk;
450 if (evt.res > 0) {
451 super.saveFile(fullFileName);
452 getDiskFileInfo();
454 }, true); // oneshot
455 editorlocked = true;
456 (new EventEditorQueryOverwriteModified(this)).post;
460 protected override void willBeDeleted (int pos, int len, int eolcount) {
461 if (len > 0) { resetIncSearchPos(); resetHighlight(); }
462 super.willBeDeleted(pos, len, eolcount);
465 protected override void willBeInserted (int pos, int len, int eolcount) {
466 if (len > 0) { resetIncSearchPos(); resetHighlight(); }
467 super.willBeInserted(pos, len, eolcount);
470 final void resetIncSearchPos (bool resethi=true) nothrow @safe @nogc {
471 if (incSearchHitPos >= 0) {
472 markLinesDirty(gb.pos2line(incSearchHitPos), 1);
473 incSearchHitPos = -1;
474 incSearchHitLen = -1;
475 if (resethi) resetHighlight();
479 final void doStartIncSearch (int sdir=0) {
480 resetIncSearchPos();
481 resetHighlight();
482 incInputActive = true;
483 incSearchDir = (sdir ? sdir : incSearchDir ? incSearchDir : 1);
484 promptActivate("incsearch", incSearchBuf);
486 incSearchBuf.length = 0;
487 incSearchBuf.assumeSafeAppend;
491 final void doNextIncSearch (bool domove=true) {
492 if (incSearchDir == 0 || incSearchBuf.length == 0) {
493 resetIncSearchPos();
494 resetHighlight();
495 return;
497 //gb.moveGapAtEnd();
498 //TODO: use `memr?chr()` here?
499 resetIncSearchPos();
500 resetHighlight();
501 if (incSearchBuf.ptr[0] != '/') {
502 // plain text
503 int pos = curpos+(domove ? incSearchDir : 0);
504 PlainMatch mt;
505 if (incSearchDir < 0) {
506 mt = findTextPlainBack(incSearchBuf, 0, pos, /*words:*/false, /*caseSens:*/true);
507 } else {
508 mt = findTextPlain(incSearchBuf, pos, textsize, /*words:*/false, /*caseSens:*/true);
510 if (!mt.empty) {
511 incSearchHitPos = mt.s;
512 incSearchHitLen = mt.e-mt.s;
513 texthi.pos = incSearchHitPos;
514 texthi.count = incSearchHitLen;
515 texthi.clr = IncSearchColor;
517 } else if (incSearchBuf.length > 2 && incSearchBuf[$-1] == '/') {
518 // regexp
519 import std.utf : byChar;
520 auto re = RegExp.create(incSearchBuf[1..$-1].byChar, SRFlags.Multiline);
521 if (!re.valid) { ttyBeep; return; }
522 Pike.Capture[2] caps;
523 bool found;
524 if (incSearchDir > 0) {
525 found = findTextRegExp(re, curpos+(domove ? 1 : 0), textsize, caps);
526 } else {
527 found = findTextRegExpBack(re, 0, curpos+(domove ? -1 : 0), caps);
529 if (found) {
530 // something was found
531 incSearchHitPos = caps[0].s;
532 incSearchHitLen = caps[0].e-caps[0].s;
533 texthi.pos = incSearchHitPos;
534 texthi.count = incSearchHitLen;
535 texthi.clr = IncSearchColor;
538 if (incSearchHitPos >= 0 && incSearchHitLen > 0) {
539 pushUndoCurPos();
540 gb.pos2xy(incSearchHitPos, cx, cy);
541 makeCurLineVisibleCentered();
542 markRangeDirty(incSearchHitPos, incSearchHitLen);
546 final void drawScrollBar () {
547 if (winx == 0) return; // it won't be visible anyway
548 if (singleline || hideSBar) return;
549 auto win = XtWindow(winx-1, winy-(hideStatus ? 0 : 1), 1, winh+(hideStatus ? 0 : 1));
550 if (win.height < 1) return; // it won't be visible anyway
551 win.fg = TtyRgb2Color!(0x00, 0x00, 0x00);
552 win.bg = (termType != TermType.linux ? TtyRgb2Color!(0x00, 0x5f, 0xaf) : TtyRgb2Color!(0x00, 0x5f, 0xcf));
553 int filled;
554 int botline = topline+winh-1;
555 if (botline >= linecount-1 || linecount == 0) {
556 filled = win.height;
557 } else {
558 filled = (win.height-1)*botline/linecount;
559 if (filled == win.height-1 && mTopLine+winh < linecount) --filled;
561 foreach (immutable y; 0..win.height) win.writeCharsAt!true(0, y, 1, (y <= filled ? ' ' : 'a'));
564 public override void drawCursor () {
565 if (dontSetCursor) return;
566 // draw prompt if it is active
567 if (mPromptActive) { mPromptInput.drawCursor(); return; }
568 int rx, ry;
569 gb.pos2xyVT(curpos, rx, ry);
570 XtWindow(winx, winy, winw, winh).gotoXY(rx-mXOfs, ry-topline);
573 public override void drawStatus () {
574 if (singleline || hideStatus) return;
575 auto win = XtWindow(winx, winy-1, winw, 1);
576 win.fg = TtyRgb2Color!(0x00, 0x00, 0x00);
577 win.bg = TtyRgb2Color!(0xb2, 0xb2, 0xb2);
578 win.writeCharsAt(0, 0, win.width, ' ');
579 import core.stdc.stdio : snprintf;
580 auto cp = curpos;
581 auto c = cast(uint)gb[cp];
582 char[512] buf = void;
583 auto sx = cx;
584 if (visualtabs) {
585 int ry;
586 gb.pos2xyVT(cp, sx, ry);
588 if (!utfuck) {
589 auto len = snprintf(buf.ptr, buf.length, " %c[%04u:%04u : 0x%08x : %u : %u] [ 0x%08x : 0x%08x ] 0x%02x %3u",
590 (textChanged ? '*' : ' '), sx+0, cy+1, cp, topline, mXOfs, bstart, bend, c, c);
591 if (len > winw) len = winw;
592 win.writeStrAt(0, 0, buf[0..len]);
593 } else {
594 dchar dch = dcharAt(cp);
595 if (dch > dchar.max) dch = 0;
596 auto len = snprintf(buf.ptr, buf.length, " %c[%04u:%04u : 0x%08x : %u : %u] [ 0x%08x : 0x%08x ] 0x%02x %3u U%04X",
597 (textChanged ? '*' : ' '), sx+0, cy+1, cp, topline, mXOfs, bstart, bend, c, c, cast(uint)dch);
598 if (len > winw) len = winw;
599 win.writeStrAt(0, 0, buf[0..len]);
601 if (utfuck) {
602 win.fb(11, 9);
603 if (readonly) win.writeCharsAt(0, 0, 1, '/'); else win.writeCharsAt(0, 0, 1, 'U');
604 } else if (readonly) {
605 win.fb(11, 9);
606 win.writeCharsAt(0, 0, 1, 'R');
610 // highlighting is done, other housekeeping is done, only draw
611 // lidx is always valid
612 // must repaint the whole line
613 // use `winXXX` vars to know window dimensions
614 //FIXME: clean this up!
615 public override void drawLine (int lidx, int yofs, int xskip) {
616 immutable vt = visualtabs;
617 immutable tabsz = gb.tabsize;
618 auto win = XtWindow(winx, winy, winw, winh);
619 auto pos = gb.line2pos(lidx);
620 int x = -xskip;
621 int y = yofs;
622 auto ts = gb.textsize;
623 bool inBlock = (bstart < bend && pos >= bstart && pos < bend);
624 bool bookmarked = isLineBookmarked(lidx);
625 if (singleline) {
626 win.color = (clrText ? clrText : TextColor);
627 if (killTextOnChar) win.color = (clrTextUnchanged ? clrTextUnchanged : TextKillColor);
628 if (inBlock) win.color = (clrBlock ? clrBlock : BlockColor);
629 } else {
630 win.color = TextColor;
631 if (bookmarked) win.color = BookmarkColor; else if (inBlock) win.color = BlockColor;
633 // if we have no highlighter, check for trailing spaces explicitly
634 bool hasHL = (hl !is null); // do we have highlighter (just a cache)
635 // look for trailing spaces even if we have a highlighter
636 int trspos = gb.line2pos(lidx+1); // this is where trailing spaces starts (will)
637 if (!singleline) while (trspos > pos && gb[trspos-1] <= ' ') --trspos;
638 bool utfucked = utfuck;
639 int bs = bstart, be = bend;
640 auto sltextClr = (singleline ?
641 (killTextOnChar ?
642 (clrTextUnchanged ? clrTextUnchanged : TextKillColor) :
643 (clrText ? clrText : TextColor)) :
644 TextColor);
645 auto blkClr = (singleline ? (clrBlock ? clrBlock : BlockColor) : BlockColor);
646 while (pos < ts) {
647 inBlock = (bs < be && pos >= bs && pos < be);
648 auto ch = gb[pos++];
649 if (ch == '\n') {
650 if (!killTextOnChar && !singleline) win.color = (hasHL ? hiColor(gb.hi(pos-1)) : TextColor); else win.color = sltextClr;
651 } else if (hasHL) {
652 // has highlighter
653 if (pos-1 >= trspos) {
654 if (ch != '\t') ch = '.';
655 if (!killTextOnChar && !singleline) win.color = TrailSpaceColor; else win.color = sltextClr;
656 } else if (ch < ' ' || ch == 127) {
657 if (!killTextOnChar && !singleline) win.color = BadColor; else win.color = sltextClr;
658 } else {
659 auto hs = gb.hi(pos-1);
660 if (!killTextOnChar && !singleline) win.color = hiColor(hs); else win.color = sltextClr;
662 } else {
663 // no highlighter
664 if (pos-1 >= trspos) {
665 if (ch != '\t') ch = '.';
666 if (!killTextOnChar && !singleline) win.color = TrailSpaceColor; else win.color = sltextClr;
667 } else if (ch < ' ' || ch == 127) {
668 if (!killTextOnChar && !singleline) win.color = BadColor; else win.color = sltextClr;
669 } else {
670 win.color = sltextClr;
673 if (!killTextOnChar && !singleline) {
674 if (bookmarked) win.color = BookmarkColor; else if (inBlock) win.color = blkClr;
675 } else {
676 if (inBlock) win.color = blkClr; else win.color = sltextClr;
678 if (ch == '\n') break;
679 if (x < winw) {
680 if (vt && ch == '\t') {
681 int ex = ((x+tabsz)/tabsz)*tabsz;
682 if (ex > 0) {
683 int sz = ex-x;
684 if (sz > 1) {
685 win.writeCharsAt(x, y, 1, '<');
686 win.writeCharsAt(x+1, y, sz-2, '-');
687 win.writeCharsAt(ex-1, y, 1, '>');
688 } else {
689 win.writeCharsAt(x, y, 1, '\t');
692 x = ex-1; // compensate the following increment
693 } else if (!utfucked) {
694 if (x >= 0) win.writeCharsAt(x, y, 1, recodeCharFrom(ch));
695 } else {
696 // utfuck
697 --pos;
698 if (x >= 0) {
699 dchar dch = dcharAt(pos);
700 if (dch > dchar.max || dch == 0xFFFD) {
701 auto oc = win.color;
702 scope(exit) win.color = oc;
703 if (!inBlock) win.color = UtfuckedColor;
704 win.writeCharsAt!true(x, y, 1, '\x7e'); // dot
705 } else {
706 win.writeCharsAt(x, y, 1, uni2koi(dch));
709 pos += gb.utfuckLenAt(pos);
711 if (++x >= winw) return;
714 if (x >= winw) return;
715 if (x < 0) x = 0;
716 if (pos >= ts) {
717 win.color = TextColor;
718 if (!killTextOnChar && !singleline) {
719 if (bookmarked) win.color = BookmarkColor; else win.color = sltextClr;
720 } else {
721 win.color = sltextClr;
724 win.writeCharsAt(x, y, winw-x, ' ');
727 // just clear line
728 // use `winXXX` vars to know window dimensions
729 public override void drawEmptyLine (int yofs) {
730 auto win = XtWindow(winx, winy, winw, winh);
731 if (singleline) {
732 win.color = (clrText ? clrText : TextColor);
733 if (killTextOnChar) win.color = (clrTextUnchanged ? clrTextUnchanged : TextKillColor);
734 } else {
735 win.color = TextColor;
737 win.writeCharsAt(0, yofs, winw, ' ');
740 public override void drawPageBegin () {
743 // check if line has only spaces (or comments, use highlighter) to begin/end (determined by dir)
744 final bool lineHasOnlySpaces (int pos, int dir) {
745 if (dir == 0) return false;
746 dir = (dir < 0 ? -1 : 1);
747 auto ts = textsize;
748 if (ts == 0) return true; // wow, rare case
749 if (pos < 0) { if (dir < 0) return true; pos = 0; }
750 if (pos >= ts) { if (dir > 0) return true; pos = ts-1; }
751 while (pos >= 0 && pos < ts) {
752 auto ch = gb[pos];
753 if (ch == '\n') break;
754 if (ch > ' ') {
755 // nonspace, check highlighting, if any
756 if (hl !is null) {
757 auto lidx = gb.pos2line(pos);
758 if (hl.fixLine(lidx)) markLinesDirty(lidx, 1); // so it won't lost dirty flag in redraw
759 if (!hiIsComment(gb.hi(pos))) return false;
760 // ok, it is comment, it's the same as whitespace
761 } else {
762 return false;
765 pos += dir;
767 return true;
770 // always starts at BOL
771 final int lineFindFirstNonSpace (int pos) {
772 auto ts = textsize;
773 if (ts == 0) return 0; // wow, rare case
774 pos = gb.line2pos(gb.pos2line(pos));
775 while (pos < ts) {
776 auto ch = gb[pos];
777 if (ch == '\n') break;
778 if (ch > ' ') {
779 // nonspace, check highlighting, if any
780 if (hl !is null) {
781 auto lidx = gb.pos2line(pos);
782 if (hl.fixLine(lidx)) markLinesDirty(lidx, 1); // so it won't lost dirty flag in redraw
783 if (!hiIsComment(gb.hi(pos))) return pos;
784 // ok, it is comment, it's the same as whitespace
785 } else {
786 return pos;
789 ++pos;
791 return pos;
794 final void drawHiBracket (int pos, int lidx, char bch, char ech, int dir, bool drawline=false) {
795 enum LineScanPages = 8;
796 int level = 1;
797 auto ts = gb.textsize;
798 auto stpos = pos;
799 int toplimit = (drawline ? mTopLine-winh*(LineScanPages-1) : mTopLine);
800 int botlimit = (drawline ? mTopLine+winh*LineScanPages : mTopLine+winh);
801 pos += dir;
802 while (pos >= 0 && pos < ts) {
803 auto ch = gb[pos];
804 if (ch == '\n') {
805 lidx += dir;
806 if (lidx < toplimit || lidx >= botlimit) return;
807 pos += dir;
808 continue;
810 if (isAnyTextChar(pos, (dir > 0))) {
811 if (ch == bch) {
812 ++level;
813 } else if (ch == ech) {
814 if (--level == 0) {
815 int rx, ry;
816 gb.pos2xyVT(pos, rx, ry);
817 if (rx >= mXOfs || rx < mXOfs+winw) {
818 if (ry >= mTopLine && ry < mTopLine+winh) {
819 auto win = XtWindow(winx, winy, winw, winh);
820 win.color = BracketColor;
821 win.writeCharsAt(rx-mXOfs, ry-mTopLine, 1, ech);
822 markLinesDirty(ry, 1);
824 // draw line with opening bracket if it is out of screen
825 if (drawline && dir < 0 && ry < mTopLine) {
826 // line start
827 int ls = pos;
828 while (ls > 0 && gb[ls-1] != '\n') --ls;
829 // skip leading spaces
830 while (ls < pos && gb[ls] <= ' ') ++ls;
831 // line end
832 int le = pos+1;
833 while (le < ts && gb[le] != '\n') ++le;
834 // remove trailing spaces
835 while (le > pos && gb[le-1] <= ' ') --le;
836 if (ls < le) {
837 auto win = XtWindow(winx+1, winy-1, winw-1, 1);
838 win.color = XtColorFB!(TtyRgb2Color!(0x40, 0x40, 0x40), TtyRgb2Color!(0xb2, 0xb2, 0xb2)); // 0,7
839 win.writeCharsAt(0, 0, winw, ' ');
840 int x = 0;
841 while (x < winw && ls < le) {
842 win.writeCharsAt(x, 0, 1, gb.utfuckAt(ls));
843 ls += gb.utfuckLenAt(ls);
844 ++x;
848 if (drawline) {
849 // draw vertical line
850 int stx, sty, ls;
851 if (dir > 0) {
852 // opening
853 // has some text after the bracket on the starting line?
854 //if (!lineHasOnlySpaces(stpos+1, 1)) return; // has text, can't draw
855 // find first non-space at the starting line
856 ls = lineFindFirstNonSpace(stpos);
857 } else if (dir < 0) {
858 // closing
859 gb.pos2xyVT(stpos, rx, ry);
860 ls = lineFindFirstNonSpace(pos);
862 gb.pos2xyVT(ls, stx, sty);
863 if (stx == rx) {
864 markLinesDirtySE(sty+1, ry-1);
865 rx -= mXOfs;
866 stx -= mXOfs;
867 ry -= mTopLine;
868 sty -= mTopLine;
869 auto win = XtWindow(winx, winy, winw, winh);
870 win.color = VLineColor;
871 win.vline(stx, sty+1, ry-sty-1);
875 return;
879 pos += dir;
883 // `fullreset` is false: keep insearch highlighting
884 protected final void resetHighlight (bool fullreset=true) nothrow @safe @nogc {
885 if (texthi.valid) {
886 texthi.count = 0;
887 fullDirty();
889 if (!fullreset) {
890 texthi.pos = incSearchHitPos;
891 texthi.count = incSearchHitLen;
892 texthi.clr = IncSearchColor;
896 protected final void drawPartHighlight (int pos, int count, uint clr) {
897 if (pos >= 0 && count > 0 && pos < gb.textsize) {
898 int rx, ry;
899 gb.pos2xyVT(pos, rx, ry);
900 //auto oldclr = xtGetColor;
901 //scope(exit) win.color = oldclr;
902 if (ry >= topline && ry < topline+winh) {
903 // visible, mark it
904 auto win = XtWindow(winx, winy, winw, winh);
905 win.color = clr;
906 if (count > gb.textsize-pos) count = gb.textsize-pos;
907 rx -= mXOfs;
908 ry -= topline;
909 //ry += winy;
910 if (!utfuck) {
911 foreach (immutable _; 0..count) {
912 if (rx >= 0 && rx < winw) win.writeCharsAt(rx, ry, 1, recodeCharFrom(gb[pos++]));
913 ++rx;
915 } else {
916 int end = pos+count;
917 while (pos < end) {
918 if (rx >= winw) break;
919 if (rx >= 0) {
920 dchar dch = dcharAt(pos);
921 if (dch > dchar.max || dch == 0xFFFD) {
922 //auto oc = xtGetColor();
923 //win.color = UtfuckedColor;
924 win.writeCharsAt!true(rx, ry, 1, '\x7e'); // dot
925 } else {
926 win.writeCharsAt(rx, ry, 1, uni2koi(dch));
929 ++rx;
930 pos += gb.utfuckLenAt(pos);
937 public override void drawPageMisc () {
938 auto pos = curpos;
939 if (isAnyTextChar(pos, false)) {
940 auto ch = gb[pos];
941 if (ch == '(') drawHiBracket(pos, cy, ch, ')', 1);
942 else if (ch == '{') drawHiBracket(pos, cy, ch, '}', 1, true);
943 else if (ch == '[') drawHiBracket(pos, cy, ch, ']', 1);
944 else if (ch == ')') drawHiBracket(pos, cy, ch, '(', -1);
945 else if (ch == '}') drawHiBracket(pos, cy, ch, '{', -1, true);
946 else if (ch == ']') drawHiBracket(pos, cy, ch, '[', -1);
948 if (texthi.valid) drawPartHighlight(texthi.pos, texthi.count, texthi.clr);
949 drawScrollBar();
952 public override void drawPageEnd () {
953 if (mPromptActive) {
954 auto win = XtWindow(winx, winy-(singleline || hideStatus ? 0 : 1), winw, 1);
955 win.color = mPromptInput.clrText;
956 win.writeCharsAt(0, 0, winw, ' ');
957 win.writeStrAt(0, 0, mPromptPrompt[0..mPromptLen]);
958 win.writeCharsAt(mPromptLen, 0, 1, ':');
959 mPromptInput.fullDirty(); // force redraw
960 mPromptInput.drawPage();
961 return;
965 //TODO: fix cx if current line was changed
966 final void doUntabify (int tabSize=2) {
967 if (mReadOnly || gb.textsize == 0) return;
968 if (tabSize < 1 || tabSize > 255) return;
969 int pos = 0;
970 auto ts = gb.textsize;
971 int curx = 0;
972 while (pos < ts && gb[pos] != '\t') {
973 if (gb[pos] == '\n') curx = 0; else ++curx;
974 ++pos;
976 if (pos >= ts) return;
977 undoGroupStart();
978 scope(exit) undoGroupEnd();
979 char[255] spaces = ' ';
980 txchanged = true;
981 while (pos < ts) {
982 // replace space
983 assert(gb[pos] == '\t');
984 int spc = tabSize-(curx%tabSize);
985 replaceText!"none"(pos, 1, spaces[0..spc]);
986 // find next tab
987 ts = gb.textsize;
988 while (pos < ts && gb[pos] != '\t') {
989 if (gb[pos] == '\n') curx = 0; else ++curx;
990 ++pos;
993 normXY();
996 //TODO: fix cx if current line was changed
997 final void doRemoveTailingSpaces () {
998 if (mReadOnly || gb.textsize == 0) return;
999 bool wasChanged = false;
1000 scope(exit) if (wasChanged) undoGroupEnd();
1001 foreach (int lidx; 0..linecount) {
1002 auto ls = gb.line2pos(lidx);
1003 auto le = gb.lineend(lidx); // points at '\n'
1004 if (gb[le] != '\n') {
1005 // for last line
1006 ++le;
1007 } else if (le-ls < 1) {
1008 continue;
1010 int count = 0;
1011 while (le > ls && gb[le-1] <= ' ') { --le; ++count; }
1012 if (count == 0) continue;
1013 if (!wasChanged) { undoGroupStart(); wasChanged = true; }
1014 deleteText!"none"(le, count);
1016 normXY();
1019 // not string, not comment
1020 // if `goingDown` is true, update highlighting
1021 final bool isAnyTextChar (int pos, bool goingDown) {
1022 if (hl is null) return true;
1023 if (pos < 0 || pos >= gb.textsize) return false;
1024 // update highlighting
1025 if (goingDown && hl !is null) {
1026 auto lidx = gb.pos2line(pos);
1027 if (hl.fixLine(lidx)) markLinesDirty(lidx, 1); // so it won't lost dirty flag in redraw
1028 // ok, it is comment, it's the same as whitespace
1030 switch (gb.hi(pos).kwtype) {
1031 case HiCommentOneLine:
1032 case HiCommentMulti:
1033 case HiChar:
1034 case HiCharSpecial:
1035 case HiString:
1036 case HiStringSpecial:
1037 case HiSQString:
1038 case HiSQStringSpecial:
1039 case HiBQString:
1040 case HiRQString:
1041 return false;
1042 default:
1044 return true;
1047 final bool isInComment (int pos) {
1048 if (hl is null || pos < 0 || pos >= gb.textsize) return false;
1049 return hiIsComment(gb.hi(pos));
1052 final bool isACGoodWordChar (int pos) {
1053 if (pos < 0 || pos >= gb.textsize) return false;
1054 if (!isWordChar(gb[pos])) return false;
1055 if (hl !is null) {
1056 // don't autocomplete in strings
1057 switch (gb.hi(pos).kwtype) {
1058 case HiNumber:
1059 case HiChar:
1060 case HiCharSpecial:
1061 case HiString:
1062 case HiStringSpecial:
1063 case HiSQString:
1064 case HiSQStringSpecial:
1065 case HiBQString:
1066 case HiRQString:
1067 return false;
1068 default:
1071 return true;
1074 final void doAutoComplete () {
1075 if (mReadOnly) return;
1077 scope(exit) {
1078 acused = 0;
1079 if (acbuffer.length > 0) {
1080 acbuffer.length = 0;
1081 acbuffer.assumeSafeAppend;
1085 void addAcToken (const(char)[] tk) {
1086 if (tk.length == 0) return;
1087 foreach (const(char)[] t; aclist[0..acused]) if (t == tk) return;
1088 if (acused >= aclist.length) return;
1089 // add to buffer
1090 auto pos = acbuffer.length;
1091 acbuffer ~= tk;
1092 aclist[acused++] = acbuffer[pos..$];
1095 import std.ascii : isAlphaNum;
1096 // get token to autocomplete
1097 auto pos = curpos;
1098 if (!isACGoodWordChar(pos-1)) return;
1099 //debug(egauto) { { import iv.vfs; auto fo = VFile("z00_ch.bin", "w"); fo.write(ch); } }
1100 bool startedInComment = isInComment(pos-1);
1101 char[128] tk = void;
1102 int tkpos = cast(int)tk.length;
1103 while (pos > 0 && isACGoodWordChar(pos-1)) {
1104 if (tkpos == 0) return;
1105 tk.ptr[--tkpos] = gb[--pos];
1107 int tklen = cast(int)tk.length-tkpos;
1108 auto tkstpos = pos;
1109 //HACK: try "std.algo"
1110 if (gb[pos-1] == '.' && gb[pos-2] == 'd' && gb[pos-3] == 't' && gb[pos-4] == 's' && !isACGoodWordChar(pos-5)) {
1111 if (tk[$-tklen..$] == "algo") {
1112 tkstpos += tklen;
1113 string ntx = "rithm";
1114 // insert new token
1115 replaceText!"end"(tkstpos, 0, ntx);
1116 return;
1119 //debug(egauto) { { import iv.vfs; auto fo = VFile("z00_tk.bin", "w"); fo.write(tk[$-tklen..$]); } }
1120 // build token list
1121 char[128] xtk = void;
1122 while (pos > 0) {
1123 while (pos > 0 && !isACGoodWordChar(pos-1)) --pos;
1124 if (pos <= 0) break;
1125 int xtp = cast(int)xtk.length;
1126 while (pos > 0 && isACGoodWordChar(pos-1)) {
1127 if (xtp > 0) {
1128 xtk.ptr[--xtp] = gb[--pos];
1129 } else {
1130 xtp = -1;
1131 --pos;
1134 if (xtp >= 0 && isInComment(pos) == startedInComment) {
1135 int xlen = cast(int)xtk.length-xtp;
1136 if (xlen > tklen) {
1137 import core.stdc.string : memcmp;
1138 if (memcmp(xtk.ptr+xtk.length-xlen, tk.ptr+tk.length-tklen, tklen) == 0) {
1139 const(char)[] tt = xtk[$-xlen..$];
1140 addAcToken(tt);
1141 if (acused >= 128) break;
1146 debug(egauto) { { import iv.vfs; auto fo = VFile("z00_list.bin", "w"); fo.writeln(list[]); } }
1147 const(char)[] acp;
1148 if (acused == 0) return;
1149 if (acused == 1) {
1150 acp = aclist[0];
1151 if (mReadOnly) return;
1152 replaceText!"end"(tkstpos, tklen, acp);
1153 } else {
1154 auto oldlk = editorlocked;
1155 addEventListener(this, (EventEditorReplyAutocompletion evt) {
1156 editorlocked = oldlk;
1157 if (evt.res.length) replaceText!"end"(evt.pos, evt.len, evt.res);
1158 }, true); // oneshot
1159 editorlocked = true;
1160 int rx, ry;
1161 gb.pos2xyVT(tkstpos, rx, ry);
1162 (new EventEditorQueryAutocompletion(this, tkstpos, tklen, FuiPoint((rx-mXOfs), (ry-mTopLine)+1), aclist[0..acused])).post;
1166 final char[] buildHelpText(this ME) () {
1167 char[] res;
1168 void buildHelpFor(UDA) () {
1169 foreach (string memn; __traits(allMembers, ME)) {
1170 static if (is(typeof(__traits(getMember, ME, memn)))) {
1171 foreach (const attr; __traits(getAttributes, __traits(getMember, ME, memn))) {
1172 static if (is(typeof(attr) == UDA)) {
1173 static if (attr.help.length && attr.key.length) {
1174 // check modifiers
1175 bool goodMode = true;
1176 foreach (const attrx; __traits(getAttributes, __traits(getMember, ME, memn))) {
1177 static if (is(attrx == TEDSingleOnly)) { if (!singleline) goodMode = false; }
1178 else static if (is(attrx == TEDMultiOnly)) { if (singleline) goodMode = false; }
1179 else static if (is(attrx == TEDEditOnly)) { if (readonly) goodMode = false; }
1180 else static if (is(attrx == TEDROOnly)) { if (!readonly) goodMode = false; }
1182 if (goodMode) {
1183 //res ~= "|";
1184 res ~= attr.key;
1185 foreach (immutable _; attr.key.length..12) res ~= ".";
1186 //res ~= "|";
1187 res ~= " ";
1188 res ~= attr.help;
1189 res ~= "\n";
1197 buildHelpFor!TEDKey;
1198 while (res.length && res[$-1] <= ' ') res = res[0..$-1];
1199 return res;
1202 protected enum Ecc { None, Eaten, Combo }
1204 // None: not valid
1205 // Eaten: exact hit
1206 // Combo: combo start
1207 // comboBuf should contain comboCount+1 keys!
1208 protected final Ecc checkKeys (const(char)[] keys) {
1209 TtyEvent k;
1210 // check if current combo prefix is ok
1211 foreach (const ref TtyEvent ck; comboBuf[0..comboCount+1]) {
1212 keys = TtyEvent.parse(k, keys);
1213 if (k.key == TtyEvent.Key.Error || k.key == TtyEvent.Key.None || k.key == TtyEvent.Key.Unknown) return Ecc.None;
1214 if (k != ck) return Ecc.None;
1216 return (keys.length == 0 ? Ecc.Eaten : Ecc.Combo);
1219 // fuck! `(this ME)` trick doesn't work here
1220 protected final Ecc doEditorCommandByUDA(ME=typeof(this)) (TtyEvent key) {
1221 import std.traits;
1222 if (key.key == TtyEvent.Key.None) return Ecc.None;
1223 if (key.key == TtyEvent.Key.Error || key.key == TtyEvent.Key.Unknown) { comboCount = 0; return Ecc.None; }
1224 bool possibleCombo = false;
1225 // temporarily add current key to combo
1226 comboBuf[comboCount] = key;
1227 // check all known combos
1228 foreach (string memn; __traits(allMembers, ME)) {
1229 static if (is(typeof(&__traits(getMember, ME, memn)))) {
1230 import std.meta : AliasSeq;
1231 alias mx = AliasSeq!(__traits(getMember, ME, memn))[0];
1232 static if (isCallable!mx && hasUDA!(mx, TEDKey)) {
1233 // check modifiers
1234 bool goodMode = true;
1235 static if (hasUDA!(mx, TEDSingleOnly)) { if (!singleline) goodMode = false; }
1236 static if (hasUDA!(mx, TEDMultiOnly)) { if (singleline) goodMode = false; }
1237 static if (hasUDA!(mx, TEDEditOnly)) { if (readonly) goodMode = false; }
1238 static if (hasUDA!(mx, TEDROOnly)) { if (!readonly) goodMode = false; }
1239 if (goodMode) {
1240 foreach (const TEDKey attr; getUDAs!(mx, TEDKey)) {
1241 auto cc = checkKeys(attr.key);
1242 if (cc == Ecc.Eaten) {
1243 // hit
1244 static if (is(ReturnType!mx == void)) {
1245 comboCount = 0; // reset combo
1246 mx();
1247 return Ecc.Eaten;
1248 } else {
1249 if (mx()) {
1250 comboCount = 0; // reset combo
1251 return Ecc.Eaten;
1254 } else if (cc == Ecc.Combo) {
1255 possibleCombo = true;
1262 // check if we can start/continue combo
1263 // combo can't start with normal char, but can include normal chars
1264 if (possibleCombo && (comboCount > 0 || key.key != TtyEvent.Key.Char)) {
1265 if (++comboCount < comboBuf.length-1) return Ecc.Combo;
1267 // if we have combo prefix, eat key unconditionally
1268 if (comboCount > 0) {
1269 comboCount = 0; // reset combo, too long, or invalid, or none
1270 return Ecc.Eaten;
1272 return Ecc.None;
1275 bool processKey (TtyEvent key) {
1276 // hack it here, so it won't interfere with normal keyboard processing
1277 if (key.key == TtyEvent.Key.PasteStart) { doPasteStart(); return true; }
1278 if (key.key == TtyEvent.Key.PasteEnd) { doPasteEnd(); return true; }
1279 if (editorlocked) return false;
1281 if (waitingInF5) {
1282 waitingInF5 = false;
1283 if (key == "enter") {
1284 if (tempBlockFileName.length) {
1285 try { doBlockRead(tempBlockFileName); } catch (Exception) {} // sorry
1288 return true;
1291 if (key.key == TtyEvent.Key.Error || key.key == TtyEvent.Key.Unknown) { comboCount = 0; return false; }
1293 if (incInputActive) {
1294 if (key.key == TtyEvent.Key.ModChar) {
1295 if (key == "^C") { incInputActive = false; resetIncSearchPos(); resetHighlight(); promptNoKillText(); return true; }
1296 if (key == "^R") { incSearchDir = 1; doNextIncSearch(); promptNoKillText(); return true; }
1297 if (key == "^V") { incSearchDir = -1; doNextIncSearch(); promptNoKillText(); return true; }
1298 //return true;
1300 if (key == "esc" || key == "enter") {
1301 incInputActive = false;
1302 promptDeactivate();
1303 resetIncSearchPos(false);
1304 //resetHighlight();
1305 return true;
1307 if (mPromptInput !is null) mPromptInput.utfuck = utfuck;
1308 promptProcessKey(key, delegate (ed) {
1309 // input buffer changed
1310 // check if it was *really* changed
1311 if (incSearchBuf.length == ed.textsize) {
1312 int pos = 0;
1313 foreach (char ch; ed[]) { if (incSearchBuf[pos] != ch) break; ++pos; }
1314 if (pos >= ed.textsize) return; // nothing was changed, so nothing to do
1316 // recollect it
1317 incSearchBuf.length = 0;
1318 incSearchBuf.assumeSafeAppend;
1319 foreach (char ch; ed[]) incSearchBuf ~= ch;
1320 doNextIncSearch(false); // don't move pointer
1322 drawStatus(); // just in case
1323 return true;
1326 //resetIncSearchPos();
1328 final switch (doEditorCommandByUDA(key)) {
1329 case Ecc.None: break;
1330 case Ecc.Combo:
1331 case Ecc.Eaten:
1332 resetHighlight(false);
1333 return true;
1336 if (key.key == TtyEvent.Key.Char) {
1337 if (readonly) return false;
1338 resetHighlight();
1339 doPutChar(cast(char)key.ch);
1340 return true;
1343 return false;
1346 bool processClick (int button, int x, int y) {
1347 if (x < 0 || y < 0 || x >= winw || y >= winh) return false;
1348 if (button != 0) return false;
1349 gotoXY(x, topline+y);
1350 return true;
1353 final:
1354 void pasteToX11 () {
1355 import std.file;
1356 import std.process;
1357 import std.regex;
1358 import std.stdio;
1360 if (!hasMarkedBlock) return;
1362 void doPaste (string cbkey) {
1363 try {
1364 string[string] moreEnv;
1365 moreEnv["K8_SUBSHELL"] = "tan";
1366 auto pp = std.process.pipeProcess(
1367 //["dmd", "-c", "-o-", "-verrors=64", "-vcolumns", fname],
1368 ["xsel", "-i", cbkey],
1369 std.process.Redirect.stderrToStdout|std.process.Redirect.stdout|std.process.Redirect.stdin,
1370 moreEnv,
1371 std.process.Config.none,
1372 null, //workdir
1374 pp.stdout.close();
1375 auto rng = markedBlockRange;
1376 foreach (char ch; rng) pp.stdin.write(ch);
1377 pp.stdin.flush();
1378 pp.stdin.close();
1379 pp.pid.wait;
1380 } catch (Exception) {}
1383 doPaste("-p");
1384 doPaste("-s");
1385 doPaste("-b");
1388 static struct PlainMatch {
1389 int s, e;
1390 @property bool empty () const pure nothrow @safe @nogc { return (s < 0 || s >= e); }
1393 // epos is not included
1394 final PlainMatch findTextPlain (const(char)[] pat, int spos, int epos, bool words, bool caseSens) {
1395 PlainMatch res;
1396 immutable ts = gb.textsize;
1397 if (pat.length == 0 || pat.length > ts) return res;
1398 if (epos <= spos || spos >= ts) return res;
1399 if (spos < 0) spos = 0;
1400 if (epos < 0) epos = 0;
1401 if (epos > ts) epos = ts;
1402 immutable bl = cast(int)pat.length;
1403 //dialogMessage!"debug"("findTextPlain", "spos=%s; epos=%s; ts=%s; curpos=%s; pat:[%s]", spos, epos, ts, curpos, pat);
1404 while (ts-spos >= bl) {
1405 if (caseSens || !pat.ptr[0].isalpha) {
1406 spos = gb.fastFindChar(spos, pat.ptr[0]);
1407 if (ts-spos < bl) break;
1409 bool found = true;
1410 if (caseSens) {
1411 foreach (int p; spos..spos+bl) if (gb[p] != pat.ptr[p-spos]) { found = false; break; }
1412 } else {
1413 foreach (int p; spos..spos+bl) if (gb[p].tolower != pat.ptr[p-spos].tolower) { found = false; break; }
1415 // check word boundaries
1416 if (found && words) {
1417 if (spos > 0 && isWordChar(gb[spos-1])) found = false;
1418 int ep = spos+bl;
1419 if (ep < ts && isWordChar(gb[ep])) found = false;
1421 //dialogMessage!"debug"("findTextPlain", "spos=%s; epos=%s; found=%s", spos, epos, found);
1422 if (found) {
1423 res.s = spos;
1424 res.e = spos+bl;
1425 break;
1427 ++spos;
1429 return res;
1432 // epos is not included
1433 final PlainMatch findTextPlainBack (const(char)[] pat, int spos, int epos, bool words, bool caseSens) {
1434 PlainMatch res;
1435 immutable ts = gb.textsize;
1436 if (pat.length == 0 || pat.length > ts) return res;
1437 if (epos <= spos || spos >= ts) return res;
1438 if (spos < 0) spos = 0;
1439 if (epos < 0) epos = 0;
1440 if (epos > ts) epos = ts;
1441 immutable bl = cast(int)pat.length;
1442 if (ts-epos < bl) epos = ts-bl;
1443 while (epos >= spos) {
1444 bool found = true;
1445 if (caseSens) {
1446 foreach (int p; epos..epos+bl) if (gb[p] != pat.ptr[p-epos]) { found = false; break; }
1447 } else {
1448 foreach (int p; epos..epos+bl) if (gb[p].tolower != pat.ptr[p-epos].tolower) { found = false; break; }
1450 if (found && words) {
1451 if (epos > 0 && isWordChar(gb[epos-1])) found = false;
1452 int ep = epos+bl;
1453 if (ep < ts && isWordChar(gb[ep])) found = false;
1455 if (found) {
1456 res.s = epos;
1457 res.e = epos+bl;
1458 break;
1460 --epos;
1462 return res;
1465 // epos is not included
1466 // caps are fixed so it can be used to index gap buffer
1467 final bool findTextRegExp (RegExp re, int spos, int epos, Pike.Capture[] caps) {
1468 Pike.Capture[1] tcaps;
1469 if (epos <= spos || spos >= textsize) return false;
1470 if (caps.length == 0) caps = tcaps;
1471 if (spos < 0) spos = 0;
1472 if (epos < 0) epos = 0;
1473 if (epos > textsize) epos = textsize;
1474 auto ctx = Pike.create(re, caps);
1475 if (!ctx.valid) return false;
1476 int res = SRes.Again;
1477 foreach (const(char)[] buf; gb.bufparts(spos)) {
1478 res = ctx.exec(buf, false);
1479 if (res < 0) {
1480 if (res != SRes.Again) return false;
1481 } else {
1482 break;
1485 if (res < 0) return false;
1486 if (spos+caps[0].s >= epos) return false;
1487 if (spos+caps[0].e > epos) return false;
1488 foreach (ref cp; caps) if (cp.s < cp.e) { cp.s += spos; cp.e += spos; }
1489 return true;
1492 // epos is not included
1493 // caps are fixed so it can be used to index gap buffer
1494 final bool findTextRegExpBack (RegExp re, int spos, int epos, Pike.Capture[] caps) {
1495 import core.stdc.string : memcpy;
1496 MemPool csave;
1497 Pike.Capture* savedCaps;
1498 Pike.Capture[1] tcaps;
1499 if (epos <= spos || spos >= textsize) return false;
1500 if (caps.length == 0) caps = tcaps;
1501 if (spos < 0) spos = 0;
1502 if (epos < 0) epos = 0;
1503 if (epos > textsize) epos = textsize;
1504 while (spos < epos) {
1505 auto ctx = Pike.create(re, caps);
1506 if (!ctx.valid) break;
1507 int res = SRes.Again;
1508 foreach (const(char)[] buf; gb.bufparts(spos)) {
1509 res = ctx.exec(buf, false);
1510 if (res < 0) {
1511 if (res != SRes.Again) return false;
1512 } else {
1513 break;
1516 if (spos+caps[0].s >= epos) break;
1517 //dialogMessage!"debug"("findTextRegexpBack", "spos=%s; epos=%s; found=%s", spos, epos, spos+caps[0].s);
1518 // save this hit
1519 if (savedCaps is null) {
1520 if (!csave.active) {
1521 csave = MemPool.create;
1522 if (!csave.active) return false; // alas
1523 savedCaps = csave.alloc!(typeof(caps[0]))(cast(uint)(caps[0].sizeof*(caps.length-1)));
1524 if (savedCaps is null) return false; // alas
1527 // fix it
1528 foreach (ref cp; caps) if (cp.s < cp.e) { cp.s += spos; cp.e += spos; }
1529 memcpy(savedCaps, caps.ptr, caps[0].sizeof*caps.length);
1530 //FIXME: should we skip the whole found match?
1531 spos = caps[0].s+1;
1533 if (savedCaps is null) return false;
1534 // restore latest match
1535 memcpy(caps.ptr, savedCaps, caps[0].sizeof*caps.length);
1536 return true;
1539 final void srrPlainStart (ref SROptions srr) {
1540 if (srr.search.length == 0) { srr.ed = null; return; }
1541 if (srr.inselection && !hasMarkedBlock) { srr.ed = null; return; }
1542 //scope(exit) fullDirty(); // to remove highlighting
1543 //bool closeGroup = false;
1544 //scope(exit) if (closeGroup) undoGroupEnd();
1545 int spos, epos;
1546 if (srr.inselection) {
1547 spos = bstart;
1548 epos = bend;
1549 } else {
1550 if (!srr.backwards) {
1551 // forward
1552 spos = curpos;
1553 epos = textsize;
1554 } else {
1555 // backward
1556 spos = 0;
1557 epos = curpos;
1560 srr.spos = spos;
1561 srr.epos = epos;
1562 srr.repcount = 0;
1563 srr.closeGroup = false;
1564 srr.cont = SROptions.Cont.Yes;
1565 srrPlainStep(srr);
1568 final void srrPlainDoSkip (ref SROptions srr) {
1569 if (!srr.backwards) srr.spos = srr.mts+1; else srr.epos = srr.mts;
1572 final void srrPlainDoReplace (ref SROptions srr) {
1573 ++srr.repcount;
1574 replaceText!"end"(srr.mts, srr.mte-srr.mts, srr.replace);
1575 // fix search range
1576 if (!srr.backwards) {
1577 // forward
1578 srr.spos = srr.mts+cast(int)srr.replace.length;
1579 srr.epos -= (srr.mte-srr.mts)-cast(int)srr.replace.length;
1580 } else {
1581 srr.epos = srr.mts;
1585 //TODO: write event-based code
1586 final void srrPlainStep (ref SROptions srr) {
1587 while (srr.spos < srr.epos) {
1588 PlainMatch mt;
1589 if (srr.backwards) {
1590 mt = findTextPlainBack(srr.search, srr.spos, srr.epos, srr.wholeword, srr.casesens);
1591 } else {
1592 mt = findTextPlain(srr.search, srr.spos, srr.epos, srr.wholeword, srr.casesens);
1594 if (mt.empty) break;
1595 srr.mts = mt.s;
1596 srr.mte = mt.e;
1597 // i found her!
1598 if (srr.nocomments && hl !is null) {
1599 auto lidx = gb.pos2line(mt.s);
1600 if (hl.fixLine(lidx)) markLinesDirty(lidx, 1); // so it won't lost dirty flag in redraw
1601 if (hiIsComment(gb.hi(mt.s))) {
1602 srrPlainDoSkip(srr);
1603 continue;
1606 if (srr.cont != SROptions.Cont.All) {
1607 bool doundo = (mt.s != curpos);
1608 if (doundo) gotoPos!true(mt.s);
1609 fullDirty();
1610 drawPage();
1611 texthi.pos = mt.s;
1612 texthi.count = mt.e-mt.s;
1613 texthi.clr = IncSearchColor;
1614 drawPartHighlight(texthi.pos, texthi.count, texthi.clr);
1615 auto oldlk = editorlocked;
1616 addEventListener(this, (EventEditorReplyReplacement evt) {
1617 editorlocked = oldlk;
1618 if (evt.opt is null) return;
1619 srr = *cast(SROptions*)evt.opt;
1620 assert(srr.ed is this);
1621 final switch (srr.cont) {
1622 case SROptions.Cont.Cancel:
1623 assert(srr.closeGroup == false);
1624 srr.ed = null;
1625 return;
1626 case SROptions.Cont.No:
1627 srrPlainDoSkip(srr);
1628 break;
1629 case SROptions.Cont.All:
1630 if (!srr.closeGroup) { undoGroupStart(); srr.closeGroup = true; }
1631 goto case SROptions.Cont.Yes;
1632 case SROptions.Cont.Yes:
1633 srrPlainDoReplace(srr);
1634 break;
1636 // do it again
1637 srrPlainStep(srr);
1638 }, true);
1639 editorlocked = true;
1640 (new EventEditorQueryReplacement(this, &srr)).post;
1641 return;
1643 // all
1644 srrPlainDoReplace(srr);
1646 srr.ed = null;
1647 if (srr.cont == SROptions.Cont.All) {
1648 import std.string : format;
1649 if (srr.closeGroup) { srr.closeGroup = false; undoGroupEnd(); }
1650 fullDirty();
1651 drawPage();
1652 (new EventEditorMessage(this, "%s replacement%s made".format(srr.repcount, (srr.repcount != 1 ? "s" : "")))).post;
1653 } else {
1654 assert(srr.closeGroup == false);
1658 final void srrRegexStart (ref SROptions srr) {
1659 import std.utf : byChar;
1660 if (srr.search.length == 0) { srr.ed = null; return; }
1661 if (srr.inselection && !hasMarkedBlock) { srr.ed = null; return; }
1662 srr.re = RegExp.create(srr.search.byChar, (srr.casesens ? 0 : SRFlags.CaseInsensitive)|SRFlags.Multiline);
1663 if (!srr.re.valid) { ttyBeep; srr.re = RegExp.init; srr.ed = null; return; }
1664 int spos, epos;
1665 if (srr.inselection) {
1666 spos = bstart;
1667 epos = bend;
1668 } else {
1669 if (!srr.backwards) {
1670 // forward
1671 spos = curpos;
1672 epos = textsize;
1673 } else {
1674 // backward
1675 spos = 0;
1676 epos = curpos;
1679 srr.spos = spos;
1680 srr.epos = epos;
1681 srr.repcount = 0;
1682 srr.closeGroup = false;
1683 srr.cont = SROptions.Cont.Yes;
1684 srrRegExpStep(srr);
1687 final void srrRegExpDoSkip (ref SROptions srr) {
1688 if (!srr.backwards) srr.spos = srr.caps[0].s+1; else srr.epos = srr.caps[0].s;
1691 final void srrRegExpDoReplace (ref SROptions srr) {
1692 int rereplace () {
1693 if (srr.newtext.length) { srr.newtext.length = 0; srr.newtext.assumeSafeAppend; }
1694 auto reps = srr.replace;
1695 int spos = 0;
1696 mainloop: while (spos < reps.length) {
1697 if ((reps[spos] == '$' || reps[spos] == '\\') && reps.length-spos > 1 && reps[spos+1].isdigit) {
1698 int n = reps[spos+1]-'0';
1699 spos += 2;
1700 if (!srr.caps[n].empty) foreach (char ch; this[srr.caps[n].s..srr.caps[n].e]) srr.newtext ~= ch;
1701 } else if ((reps[spos] == '$' || reps[spos] == '\\') && reps.length-spos > 2 && reps[spos+1] == '{' && reps[spos+2].isdigit) {
1702 bool toupper, tolower, capitalize, uncapitalize;
1703 spos += 2;
1704 int n = 0;
1705 // parse number
1706 while (spos < reps.length && reps[spos].isdigit) n = n*10+reps[spos++]-'0';
1707 while (spos < reps.length && reps[spos] != '}') {
1708 switch (reps[spos++]) {
1709 case 'u': case 'U': toupper = true; tolower = false; break;
1710 case 'l': case 'L': tolower = true; toupper = false; break;
1711 case 'C': capitalize = true; break;
1712 case 'c': uncapitalize = true; break;
1713 default: // ignore other flags
1716 if (spos < reps.length && reps[spos] == '}') ++spos;
1717 if (n < srr.caps.length && !srr.caps[n].empty) {
1718 int tp = srr.caps[n].s, ep = srr.caps[n].e;
1719 char ch = gb[tp++];
1720 if (capitalize || toupper) ch = ch.toupper;
1721 else if (uncapitalize || tolower) ch = ch.tolower;
1722 srr.newtext ~= ch;
1723 while (tp < ep) {
1724 ch = gb[tp++];
1725 if (toupper) ch = ch.toupper;
1726 else if (tolower) ch = ch.tolower;
1727 srr.newtext ~= ch;
1730 } else if (reps[spos] == '\\' && reps.length-spos > 1) {
1731 spos += 2;
1732 switch (reps[spos-1]) {
1733 case 't': srr.newtext ~= '\t'; break;
1734 case 'n': srr.newtext ~= '\n'; break;
1735 case 'r': srr.newtext ~= '\r'; break;
1736 case 'a': srr.newtext ~= '\a'; break;
1737 case 'b': srr.newtext ~= '\b'; break;
1738 case 'e': srr.newtext ~= '\x1b'; break;
1739 case 'x': case 'X':
1740 if (reps.length-spos < 1) break mainloop;
1741 int n = digitInBase(reps[spos], 16);
1742 if (n < 0) break;
1743 ++spos;
1744 if (reps.length-spos > 0 && digitInBase(reps[spos], 16) >= 0) {
1745 n = n*16+digitInBase(reps[spos], 16);
1746 ++spos;
1748 srr.newtext ~= cast(char)n;
1749 break;
1750 default:
1751 srr.newtext ~= reps[spos-1];
1752 break;
1754 } else {
1755 srr.newtext ~= reps[spos++];
1758 replaceText!"end"(srr.caps[0].s, srr.caps[0].e-srr.caps[0].s, srr.newtext);
1759 return cast(int)srr.newtext.length;
1762 ++srr.repcount;
1763 int replen = rereplace();
1764 // fix search range
1765 if (!srr.backwards) {
1766 srr.spos = srr.caps[0].s+(replen ? replen : 1);
1767 srr.epos += replen-(srr.caps[0].e-srr.caps[0].s);
1768 } else {
1769 srr.epos = srr.caps[0].s;
1773 final void srrRegExpStep (ref SROptions srr) {
1774 while (srr.spos < srr.epos) {
1775 bool found;
1776 if (srr.backwards) {
1777 found = findTextRegExpBack(srr.re, srr.spos, srr.epos, srr.caps[]);
1778 } else {
1779 found = findTextRegExp(srr.re, srr.spos, srr.epos, srr.caps[]);
1781 if (!found) break;
1782 // i found her!
1783 if (srr.nocomments && hl !is null) {
1784 auto lidx = gb.pos2line(srr.caps[0].s);
1785 if (hl.fixLine(lidx)) markLinesDirty(lidx, 1); // so it won't lost dirty flag in redraw
1786 if (hiIsComment(gb.hi(srr.caps[0].s))) {
1787 srrRegExpDoSkip(srr);
1788 continue;
1791 if (srr.cont != SROptions.Cont.All) {
1792 bool doundo = (srr.caps[0].s != curpos);
1793 if (doundo) gotoPos!true(srr.caps[0].s);
1794 fullDirty();
1795 drawPage();
1796 texthi.pos = srr.caps[0].s;
1797 texthi.count = srr.caps[0].e-srr.caps[0].s;
1798 texthi.clr = IncSearchColor;
1799 drawPartHighlight(texthi.pos, texthi.count, texthi.clr);
1800 auto oldlk = editorlocked;
1801 addEventListener(this, (EventEditorReplyReplacement evt) {
1802 editorlocked = oldlk;
1803 if (evt.opt is null) return;
1804 srr = *cast(SROptions*)evt.opt;
1805 assert(srr.ed is this);
1806 final switch (srr.cont) {
1807 case SROptions.Cont.Cancel:
1808 assert(srr.closeGroup == false);
1809 srr.ed = null;
1810 return;
1811 case SROptions.Cont.No:
1812 srrRegExpDoSkip(srr);
1813 break;
1814 case SROptions.Cont.All:
1815 if (!srr.closeGroup) { undoGroupStart(); srr.closeGroup = true; }
1816 goto case SROptions.Cont.Yes;
1817 case SROptions.Cont.Yes:
1818 srrRegExpDoReplace(srr);
1819 break;
1821 // do it again
1822 srrRegExpStep(srr);
1823 }, true);
1824 editorlocked = true;
1825 (new EventEditorQueryReplacement(this, &srr)).post;
1826 return;
1828 // all
1829 srrRegExpDoReplace(srr);
1831 srr.ed = null;
1832 if (srr.cont == SROptions.Cont.All) {
1833 import std.string : format;
1834 if (srr.closeGroup) { srr.closeGroup = false; undoGroupEnd(); }
1835 fullDirty();
1836 drawPage();
1837 (new EventEditorMessage(this, "%s replacement%s made".format(srr.repcount, (srr.repcount != 1 ? "s" : "")))).post;
1838 } else {
1839 assert(srr.closeGroup == false);
1843 final:
1844 void processWordWith (scope char delegate (char ch) dg) {
1845 if (dg is null) return;
1846 bool undoAdded = false;
1847 scope(exit) if (undoAdded) undoGroupEnd();
1848 auto pos = curpos;
1849 if (!isWordChar(gb[pos])) return;
1850 // find word start
1851 while (pos > 0 && isWordChar(gb[pos-1])) --pos;
1852 while (pos < gb.textsize) {
1853 auto ch = gb[pos];
1854 if (!isWordChar(gb[pos])) break;
1855 auto nc = dg(ch);
1856 if (ch != nc) {
1857 if (!undoAdded) { undoAdded = true; undoGroupStart(); }
1858 replaceText!"none"(pos, 1, (&nc)[0..1]);
1860 ++pos;
1862 gotoPos(pos);
1865 final:
1866 @TEDMultiOnly mixin TEDImpl!("Up", q{ doUp(); });
1867 @TEDMultiOnly mixin TEDImpl!("S-Up", q{ doUp(true); });
1868 @TEDMultiOnly mixin TEDImpl!("^Up", q{ doScrollUp(); });
1869 @TEDMultiOnly mixin TEDImpl!("S-^Up", q{ doScrollUp(true); });
1870 @TEDMultiOnly mixin TEDImpl!("Down", q{ doDown(); });
1871 @TEDMultiOnly mixin TEDImpl!("S-Down", q{ doDown(true); });
1872 @TEDMultiOnly mixin TEDImpl!("^Down", q{ doScrollDown(); });
1873 @TEDMultiOnly mixin TEDImpl!("S-^Down", q{ doScrollDown(true); });
1875 mixin TEDImpl!("Left", q{ doLeft(); });
1876 mixin TEDImpl!("S-Left", q{ doLeft(true); });
1878 mixin TEDImpl!("^Left", q{ doWordLeft(); });
1879 mixin TEDImpl!("S-^Left", q{ doWordLeft(true); });
1880 mixin TEDImpl!("Right", q{ doRight(); });
1881 mixin TEDImpl!("S-Right", q{ doRight(true); });
1882 mixin TEDImpl!("^Right", q{ doWordRight(); });
1883 mixin TEDImpl!("S-^Right", q{ doWordRight(true); });
1885 @TEDMultiOnly mixin TEDImpl!("PageUp", q{ doPageUp(); });
1886 @TEDMultiOnly mixin TEDImpl!("S-PageUp", q{ doPageUp(true); });
1887 @TEDMultiOnly mixin TEDImpl!("^PageUp", q{ doTextTop(); });
1888 @TEDMultiOnly mixin TEDImpl!("S-^PageUp", q{ doTextTop(true); });
1889 @TEDMultiOnly mixin TEDImpl!("PageDown", q{ doPageDown(); });
1890 @TEDMultiOnly mixin TEDImpl!("S-PageDown", q{ doPageDown(true); });
1891 @TEDMultiOnly mixin TEDImpl!("^PageDown", q{ doTextBottom(); });
1892 @TEDMultiOnly mixin TEDImpl!("S-^PageDown", q{ doTextBottom(true); });
1893 mixin TEDImpl!("Home", q{ doHome(); });
1894 mixin TEDImpl!("S-Home", q{ doHome(true, true); });
1895 @TEDMultiOnly mixin TEDImpl!("^Home", q{ doPageTop(); });
1896 @TEDMultiOnly mixin TEDImpl!("S-^Home", q{ doPageTop(true); });
1897 mixin TEDImpl!("End", q{ doEnd(); });
1898 mixin TEDImpl!("S-End", q{ doEnd(true); });
1899 @TEDMultiOnly mixin TEDImpl!("^End", q{ doPageBottom(); });
1900 @TEDMultiOnly mixin TEDImpl!("S-^End", q{ doPageBottom(true); });
1902 @TEDEditOnly mixin TEDImpl!("Backspace", q{ doBackspace(); });
1903 @TEDSingleOnly @TEDEditOnly mixin TEDImpl!("M-Backspace", "delete previous word", q{ doDeleteWord(); });
1904 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("M-Backspace", "delete previous word or unindent", q{ doBackByIndent(); });
1906 mixin TEDImpl!("Delete", q{ doDelete(); });
1907 mixin TEDImpl!("^Insert", "copy block to clipboard file, reset block mark", q{ if (tempBlockFileName.length == 0) return; doBlockWrite(tempBlockFileName); doBlockResetMark(); });
1909 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("Enter", q{ doPutChar('\n'); });
1910 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("M-Enter", "split line without autoindenting", q{ doLineSplit(false); });
1912 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("F2", "save file", q{ saveFile(fullFileName); });
1913 mixin TEDImpl!("F3", "start/stop/reset block marking", q{ doBlockMark(); });
1914 mixin TEDImpl!("^F3", "reset block mark", q{ doBlockResetMark(); });
1915 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("F4", "search and relace text", q{
1916 if (srrOptions.ed !is null) return; // in progress
1917 srrOptions.ed = this;
1918 auto oldlk = editorlocked;
1919 addEventListener(this, (EventEditorReplySR evt) {
1920 editorlocked = oldlk;
1921 if (!evt.proceed) { srrOptions.ed = null; return; }
1922 assert(evt.opt !is null);
1923 srrOptions = *cast(SROptions*)evt.opt;
1924 if (srrOptions.type == SROptions.Type.Normal) srrPlainStart(srrOptions);
1925 if (srrOptions.type == SROptions.Type.Regex) srrRegexStart(srrOptions);
1926 }, true);
1927 editorlocked = true;
1928 (new EventEditorQuerySR(this, &srrOptions)).post;
1930 @TEDEditOnly mixin TEDImpl!("F5", "copy block", q{ doBlockCopy(); });
1931 mixin TEDImpl!("^F5", "copy block to clipboard file", q{ if (tempBlockFileName.length == 0) return; doBlockWrite(tempBlockFileName); });
1932 @TEDEditOnly mixin TEDImpl!("S-F5", "insert block from clipboard file", q{ if (tempBlockFileName.length == 0) return; waitingInF5 = true; });
1933 @TEDEditOnly mixin TEDImpl!("F6", "move block", q{ doBlockMove(); });
1934 @TEDEditOnly mixin TEDImpl!("F8", "delete block", q{ doBlockDelete(); });
1936 mixin TEDImpl!("^A", "move to line start", q{ doHome(); });
1937 mixin TEDImpl!("^E", "move to line end", q{ doEnd(); });
1939 @TEDMultiOnly mixin TEDImpl!("M-I", "jump to previous bookmark", q{ doBookmarkJumpUp(); });
1940 @TEDMultiOnly mixin TEDImpl!("M-J", "jump to next bookmark", q{ doBookmarkJumpDown(); });
1941 @TEDMultiOnly mixin TEDImpl!("M-K", "toggle bookmark", q{ doBookmarkToggle(); });
1942 @TEDMultiOnly mixin TEDImpl!("M-L", "goto line", q{ (new EventEditorQueryGotoLine(this)).post; });
1944 @TEDEditOnly mixin TEDImpl!("M-C", "capitalize word", q{
1945 bool first = true;
1946 processWordWith((char ch) {
1947 if (first) { first = false; ch = ch.toupper; }
1948 return ch;
1951 @TEDEditOnly mixin TEDImpl!("M-Q", "lowercase word", q{ processWordWith((char ch) => ch.tolower); });
1952 @TEDEditOnly mixin TEDImpl!("M-U", "uppercase word", q{ processWordWith((char ch) => ch.toupper); });
1954 @TEDMultiOnly mixin TEDImpl!("M-S-L", "force center current line", q{ makeCurLineVisibleCentered(true); });
1955 @TEDMultiOnly mixin TEDImpl!("^R", "continue incremental search, forward", q{ incSearchDir = 1; if (incSearchBuf.length == 0 && !incInputActive) doStartIncSearch(1); else doNextIncSearch(); });
1956 @TEDEditOnly mixin TEDImpl!("^U", "undo", q{ doUndo(); });
1957 @TEDEditOnly mixin TEDImpl!("M-S-U", "redo", q{ doRedo(); });
1958 @TEDMultiOnly mixin TEDImpl!("^V", "continue incremental search, backward", q{ incSearchDir = -1; if (incSearchBuf.length == 0 && !incInputActive) doStartIncSearch(-1); else doNextIncSearch(); });
1959 @TEDEditOnly mixin TEDImpl!("^W", "remove previous word", q{ doDeleteWord(); });
1960 @TEDEditOnly mixin TEDImpl!("^Y", "remove current line", q{ doKillLine(); });
1961 @TEDMultiOnly mixin TEDImpl!("^_", "start new incremental search, forward", q{ doStartIncSearch(1); }); // ctrl+slash, actually
1962 @TEDMultiOnly mixin TEDImpl!("^\\", "start new incremental search, backward", q{ doStartIncSearch(-1); });
1964 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("Tab", q{ doPutText(" "); });
1965 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("M-Tab", "autocomplete word", q{ doAutoComplete(); });
1966 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-Tab", "indent block", q{ doIndentBlock(); });
1967 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-S-Tab", "unindent block", q{ doUnindentBlock(); });
1969 mixin TEDImpl!("M-S-c", "copy block to X11 selections (all three)", q{ pasteToX11(); doBlockResetMark(); });
1971 mixin TEDImpl!("M-E", "select codepage", q{ (new EventEditorQueryCodePage(this, (utfuck ? 3 : codepage))).post; });
1973 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("^K ^I", "indent block", q{ doIndentBlock(); });
1974 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("^K ^U", "unindent block", q{ doUnindentBlock(); });
1975 @TEDEditOnly mixin TEDImpl!("^K ^E", "clear from cursor to EOL", q{ doKillToEOL(); });
1976 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("^K Tab", "indent block", q{ doIndentBlock(); });
1977 @TEDEditOnly mixin TEDImpl!("^K M-Tab", "untabify", q{ doUntabify(gb.tabsize ? gb.tabsize : 2); }); // alt+tab: untabify
1978 @TEDEditOnly mixin TEDImpl!("^K C-space", "remove trailing spaces", q{ doRemoveTailingSpaces(); });
1979 mixin TEDImpl!("^K ^T", /*"toggle \"visual tabs\" mode",*/ q{ visualtabs = !visualtabs; });
1981 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("^K ^B", q{ doSetBlockStart(); });
1982 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("^K ^K", q{ doSetBlockEnd(); });
1984 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("^K ^C", q{ doBlockCopy(); });
1985 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("^K ^M", q{ doBlockMove(); });
1986 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("^K ^Y", q{ doBlockDelete(); });
1987 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("^K ^H", q{ doBlockResetMark(); });
1988 // fuckin' vt100!
1989 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("^K Backspace", q{ doBlockResetMark(); });
1991 @TEDEditOnly mixin TEDImpl!("^Q Tab", q{ doPutChar('\t'); });
1992 mixin TEDImpl!("^Q ^U", "toggle utfuck mode", q{ utfuck = !utfuck; }); // ^Q^U: switch utfuck mode
1993 mixin TEDImpl!("^Q 1", "switch to koi8", q{ utfuck = false; codepage = CodePage.koi8u; fullDirty(); });
1994 mixin TEDImpl!("^Q 2", "switch to cp1251", q{ utfuck = false; codepage = CodePage.cp1251; fullDirty(); });
1995 mixin TEDImpl!("^Q 3", "switch to cp866", q{ utfuck = false; codepage = CodePage.cp866; fullDirty(); });
1996 mixin TEDImpl!("^Q ^B", "go to block start", q{ if (hasMarkedBlock) gotoPos!true(bstart); lastBGEnd = false; });
1998 @TEDMultiOnly mixin TEDImpl!("^Q ^F", "incremental search current word", q{
1999 auto pos = curpos;
2000 if (!isWordChar(gb[pos])) return;
2001 // deactivate prompt
2002 if (incInputActive) {
2003 incInputActive = false;
2004 promptDeactivate();
2005 resetIncSearchPos();
2006 resetHighlight();
2008 // collect word
2009 while (pos > 0 && isWordChar(gb[pos-1])) --pos;
2010 incSearchBuf.length = 0;
2011 incSearchBuf.assumeSafeAppend;
2012 while (pos < gb.textsize && isWordChar(gb[pos])) incSearchBuf ~= gb[pos++];
2013 incSearchDir = 1;
2014 // get current word
2015 doNextIncSearch();
2018 mixin TEDImpl!("^Q ^K", "go to block end", q{ if (hasMarkedBlock) gotoPos!true(bend); lastBGEnd = true; });
2019 mixin TEDImpl!("^Q ^T", "set tab size", q{ (new EventEditorQueryTabSize(this, tabsize)).post; });
2021 @TEDMultiOnly @TEDROOnly mixin TEDImpl!("Space", q{ doPageDown(); });
2022 @TEDMultiOnly @TEDROOnly mixin TEDImpl!("^Space", q{ doPageUp(); });