some updates
[iv.d.git] / editline.d
blob5e8913f1ada14b9530b894aaea4975baeb866263
1 /* Simple readline/editline replacement. Deliberately non-configurable.
3 * Written by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
4 * Understanding is not required. Only obedience.
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, version 3 of the License ONLY.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 module iv.editline /*is aliced*/;
20 import iv.alice;
21 import iv.rawtty;
22 import iv.strex;
25 // ////////////////////////////////////////////////////////////////////////// //
26 class EditLine {
27 public:
28 final class Line {
29 public:
30 enum MaxLen = 4096;
32 private:
33 char[MaxLen] cline;
34 int lpos; // cursor position: [0..len]
35 int llen; // line length (line can be longer)
36 //uint ofs; // used in text draw
38 public:
39 this () {}
41 // cursor position
42 @property int pos () const pure nothrow @safe @nogc { pragma(inline, true); return lpos; }
43 @property void pos (int npos) pure nothrow @safe @nogc { if (npos < 0) npos = 0; if (npos > llen) npos = llen; lpos = npos; }
44 void movePos (int delta) {
45 if (delta < -MaxLen) delta = -MaxLen;
46 if (delta > MaxLen) delta = MaxLen;
47 pos = lpos+delta;
50 // line length
51 @property int length () const pure nothrow @safe @nogc { pragma(inline, true); return llen; }
53 char opIndex (long pos) const pure nothrow @trusted @nogc { pragma(inline, true); return (pos >= 0 && pos < llen ? cline.ptr[cast(uint)pos] : 0); }
54 const(char)[] opIndex() (in auto ref WordPos wp) const pure nothrow @safe @nogc { pragma(inline, true); return this[wp.s..wp.e]; }
56 // do not slice it, buffer WILL change
57 const(char)[] opSlice () const pure nothrow @safe @nogc { pragma(inline, true); return cline[0..llen]; }
58 const(char)[] opSlice (long lo, long hi) const pure nothrow @safe @nogc {
59 //pragma(inline, true);
60 if (lo >= hi) return null;
61 if (hi <= 0 || lo >= llen) return null;
62 if (lo < 0) lo = 0;
63 if (hi > llen) hi = llen;
64 return cline[cast(uint)lo..cast(uint)hi];
67 // clear line
68 void clear () pure nothrow @trusted @nogc { lpos = llen = 0; }
70 // crop line at current position
71 void crop () pure nothrow @trusted @nogc { llen = lpos; }
73 // set current line, move cursor to end; crop if length is too big
74 // return `false` if new line was cropped
75 bool set (const(char)[] s...) pure nothrow @trusted @nogc {
76 bool res = true;
77 if (s.length > MaxLen) { s = s[0..MaxLen]; res = false; }
78 if (s.length) cline[0..s.length] = s[];
79 lpos = llen = cast(int)s.length;
80 return res;
83 // insert chars at cursor position, move cursor
84 // return `false` if there is no room, line is not modified in this case
85 bool insert (const(char)[] s...) pure nothrow @trusted @nogc {
86 if (s.length == 0) return true;
87 if (s.length > MaxLen || llen+s.length > MaxLen) return false;
88 // make room
89 if (lpos < llen) {
90 import core.stdc.string : memmove;
91 memmove(cline.ptr+lpos+cast(int)s.length, cline.ptr+lpos, llen-lpos);
93 llen += cast(int)s.length;
94 // copy
95 cline[lpos..lpos+cast(int)s.length] = s[];
96 lpos += cast(int)s.length;
97 return true;
100 // replace chars at cursor position, move cursor; does appending
101 // return `false` if there is no room, line is not modified in this case
102 bool replace (const(char)[] s...) pure nothrow @trusted @nogc {
103 if (s.length == 0) return true;
104 if (s.length > MaxLen || lpos+s.length > MaxLen) return false;
105 // replace
106 cline[lpos..lpos+cast(int)s.length] = s[];
107 lpos += cast(int)s.length;
108 if (llen < lpos) llen = lpos;
109 return true;
112 // remove chars at cursor position (delete), don't move cursor
113 void remove (int len) pure nothrow @trusted @nogc {
114 if (len < 1 || lpos >= llen) return;
115 if (len > MaxLen) len = MaxLen;
116 if (lpos+len >= llen) {
117 // strip
118 llen = lpos;
119 } else {
120 import core.stdc.string : memmove;
121 memmove(cline.ptr+lpos, cline.ptr+lpos+len, llen-(lpos+len));
122 llen -= len;
123 assert(lpos < llen);
127 // delete chars at cursor position (backspace), move cursor
128 void backspace (int len) pure nothrow @trusted @nogc {
129 if (len < 1 || lpos < 1) return;
130 if (len > lpos) len = lpos;
131 lpos -= len;
132 remove(len);
135 // //// something to make programmer's life easier //// //
137 static struct WordPos {
138 int s, e; // start and end position, suitable for slicing
139 @property bool valid () const pure nothrow @safe @nogc { pragma(inline, true); return (s >= 0 && s < e); }
140 @property int length () const pure nothrow @safe @nogc { pragma(inline, true); return e-s; }
143 // word positions range
144 auto words () inout pure nothrow @trusted @nogc {
145 static struct Result {
146 private:
147 const Line ln;
148 WordPos wp;
149 this (in Line aln) pure nothrow @safe @nogc { ln = aln; popFront(); }
150 this() (in Line aln, in auto ref WordPos awp) pure nothrow @safe @nogc { ln = aln; wp.s = awp.s; wp.e = awp.e; }
151 public:
152 @property const(char)[] word () const pure nothrow @safe @nogc { pragma(inline, true); return ln[wp.s..wp.e]; }
153 @property WordPos front () const pure nothrow @safe @nogc { pragma(inline, true); return WordPos(wp.s, wp.e); }
154 @property bool empty () const pure nothrow @safe @nogc { pragma(inline, true); return (wp.s >= ln.llen); }
155 @property auto save () const pure nothrow @safe @nogc { pragma(inline, true); return Result(ln, wp); }
156 @property void popFront () pure nothrow @trusted @nogc {
157 if (wp.s >= ln.llen) return;
158 wp.s = wp.e;
159 while (wp.s < ln.llen && ln.cline.ptr[wp.s] <= ' ') ++wp.s;
160 wp.e = wp.s;
161 if (wp.e >= ln.llen) return;
162 char qch = ln.cline.ptr[wp.e];
163 if (qch == '"' || qch == '\'' || qch == '`') {
164 // quoted
165 ++wp.e;
166 while (wp.e < ln.llen) {
167 char ch = ln.cline.ptr[wp.e++];
168 if (ch == '\\') {
169 if (wp.e < ln.llen) ++wp.e;
170 } else if (ch == qch) {
171 break;
174 } else {
175 while (wp.e < ln.llen && ln.cline.ptr[wp.e] > ' ') ++wp.e;
179 return Result(this);
182 int wordCount () const pure nothrow @trusted @nogc {
183 int res = 0;
184 auto ww = words();
185 while (!ww.empty) { ++res; ww.popFront(); }
186 return res;
189 const(char)[] word (int idx) const pure nothrow @trusted @nogc {
190 if (idx < 0) return null;
191 auto ww = words();
192 while (idx > 0 && !ww.empty) { --idx; ww.popFront(); }
193 if (!ww.empty) return this[ww.front.s..ww.front.e];
194 return null;
197 // assume that `pos` is inside word, return `true` if word starts with quote
198 bool wordQuoted (int pos) const pure nothrow @trusted @nogc {
199 foreach (const ref wp; words) {
200 if (pos >= wp.s && pos < wp.e) {
201 // our word
202 char ch = this[wp.s];
203 return (ch == '"' || ch == '\'' || ch == '`');
206 return false;
209 // we can autocomplete empty line or any word if we're at it's end
210 bool canAutocomplete () const pure nothrow @trusted @nogc {
211 if (llen == 0) return true;
212 if (llen == MaxLen) return false;
213 if (lpos == 0) return false; // we have some text, so can't
214 if (lpos == llen) return (cline.ptr[lpos-1] <= ' ' || !wordQuoted(lpos-1));
215 // end of word?
216 return (cline.ptr[lpos] <= ' ' && cline.ptr[lpos-1] > ' ' && !wordQuoted(lpos-1));
219 // get word number for autocompletion
220 int acWordNum () const pure nothrow @trusted @nogc {
221 if (!canAutocomplete) return -1;
222 if (lpos == llen && cline.ptr[lpos-1] <= ' ') return wordCount;
223 int idx;
224 foreach (ref wp; words) {
225 if (lpos >= wp.s && lpos <= wp.e) return idx;
226 ++idx;
228 return idx;
231 // get word for autocompletion
232 WordPos acWordPos () const pure nothrow @trusted @nogc {
233 if (!canAutocomplete) return WordPos();
234 if (lpos == llen && cline.ptr[lpos-1] <= ' ') return WordPos(llen, llen);
235 foreach (ref wp; words) if (lpos >= wp.s && lpos <= wp.e) return wp;
236 return WordPos(llen, llen);
239 // replace current with new
240 bool acWordReplace (const(char)[] w, bool addSpace=false) pure nothrow @trusted @nogc {
241 if (w.length == 0 || w.length > MaxLen || !canAutocomplete) return false;
242 foreach (const ref wp; words) {
243 if (lpos >= wp.s && lpos <= wp.e) {
244 // check if we should add space
245 if (addSpace) addSpace = (wp.e >= llen || cline.ptr[wp.e] > ' ');
246 int addlen = (addSpace ? 1 : 0);
247 if (llen-wp.length+w.length+addlen > MaxLen) return false;
248 backspace(wp.length);
249 insert(w);
250 if (addSpace) insert(' ');
251 return true;
254 return false;
257 // replace current word with new, add space
258 bool acWordReplaceSpaced (const(char)[] w) pure nothrow @trusted @nogc { return acWordReplace(w, true); }
261 public:
262 enum Result {
263 Normal,
264 CtrlC,
265 CtrlD,
268 uint historyLimit = 512;
269 string[] history;
271 protected:
272 Line curline;
273 char[] promptbuf;
275 public:
276 this () { promptbuf = ">".dup; curline = new Line(); }
278 final @property inout(Line) line () inout pure nothrow @safe @nogc { pragma(inline, true); return curline; }
280 // const(char)[] returns slice of internal buffer, don't store it!
281 final @property T get(T=string) () if (is(T : const(char)[])) {
282 static if (is(T == string)) return curline[].idup;
283 else static if (is(T == const(char)[])) return curline[];
284 else return curline[].dup;
287 final @property string prompt () { return promptbuf.idup; }
288 final @property prompt (const(char)[] pt) {
289 promptbuf.length = 0;
290 promptbuf.assumeSafeAppend;
291 if (pt.length > 61) { promptbuf ~= "..."; pt = pt[$-61..$]; }
292 promptbuf ~= pt;
295 final void pushCurrentToHistory () { pushToHistory(get!string); }
297 final void pushToHistory(T : const(char)[]) (T s) {
298 static if (is(T == typeof(null))) string hs = null;
299 else static if (is(T == string)) string hs = s.xstrip;
300 else string hs = s.xstrip.idup;
301 if (hs.length == 0) return;
302 // find duplicate and remove it
303 foreach (immutable idx, string st; history) {
304 if (st.xstrip == hs) {
305 // i found her!
306 // remove
307 foreach (immutable c; idx+1..history.length) history[c-1] = history[c];
308 // move
309 foreach_reverse (immutable c; 1..history.length) history[c] = history[c-1];
310 // set
311 history[0] = hs;
312 // done
313 return;
316 if (history.length > historyLimit) history.length = historyLimit;
317 else if (history.length < historyLimit) history.length += 1;
318 foreach_reverse (immutable c; 1..history.length) history[c] = history[c-1];
319 history[0] = hs;
322 final Result readline (const(char)[] initval=null) {
323 char[Line.MaxLen] lastInput; // stored for history walks
324 uint lastLen; // stored for history walks
325 int hpos = -1; // -1: current
326 int curofs; // output offset
328 void fixCurLine () {
329 hpos = -1;
330 lastLen = 0;
333 void drawLine () {
334 auto wdt = ttyWidth-1;
335 if (wdt < 16) wdt = 16;
336 const(char)[] cline = (hpos < 0 ? curline[] : history[hpos]);
337 if (wdt <= promptbuf.length+3) wdt = 3; else wdt -= cast(int)promptbuf.length;
338 int wvis = (wdt < 10 ? wdt-1 : 8);
339 int cpos = curline.pos;
340 // special handling for negative offset: it means "make left part visible"
341 if (curofs < 0) {
342 // is cursor at eol? special handling
343 if (cpos == curline.length) {
344 curofs = cpos-wdt;
345 if (curofs < 0) curofs = 0;
346 } else {
347 // make wvis char after cursor visible
348 curofs = cpos-wdt+wvis;
349 if (curofs < 0) curofs = 0;
350 // i did something wrong here...
351 if (curofs >= curline.length) {
352 curofs = curline.length-wdt;
353 if (curofs < 0) curofs = 0;
356 } else {
357 // is cursor too far at left?
358 if (cpos < curofs) {
359 // make wvis left chars visible
360 curofs = cpos-wvis;
361 if (curofs < 0) curofs = 0;
363 // is cursor too far at right?
364 if (curofs+wdt <= cpos) {
365 // make wvis right chars visible
366 curofs = cpos-wdt+wvis;
367 if (curofs < 0) curofs = 0;
368 if (curofs >= curline.length) {
369 curofs = curline.length-wdt;
370 if (curofs < 0) curofs = 0;
374 assert(curofs >= 0 && curofs <= curline.length);
375 int end = curofs+wdt;
376 if (end > curline.length) end = curline.length;
377 wrt("\r");
378 wrt(promptbuf);
379 wrt(curline[curofs..end]);
380 if (cpos == end) {
381 wrt("\x1b[K");
382 } else {
383 wrt("\x1b[K\r\x1b[");
384 wrtuint(promptbuf.length+(cpos-curofs));
385 wrt("C");
389 curofs = 0;
390 curline.clear();
391 if (initval.length) curline.set(initval);
393 auto ttymode = ttyGetMode();
394 scope(exit) ttySetMode(ttymode);
395 ttySetRaw();
397 void delPrevWord () {
398 if (curline.pos > 0) {
399 fixCurLine();
400 while (curline.pos > 0 && curline[curline.pos-1] <= ' ') curline.backspace(1);
401 while (curline.pos > 0 && curline[curline.pos-1] > ' ') curline.backspace(1);
405 for (;;) {
406 drawLine();
407 auto key = ttyReadKey();
408 if (key.key == TtyEvent.Key.Error) { curline.clear(); return Result.CtrlD; }
409 if (key.key == TtyEvent.Key.Unknown) continue;
410 if (key.key == TtyEvent.Key.ModChar) {
411 if (!key.alt && key.ctrl && !key.shift && key.ch == 'W') { delPrevWord(); continue; }
412 if (!key.alt && key.ctrl && !key.shift && key.ch == 'C') { curline.clear(); return Result.CtrlC; }
413 if (!key.alt && key.ctrl && !key.shift && key.ch == 'D') { curline.clear(); return Result.CtrlD; }
414 if (!key.alt && key.ctrl && !key.shift && key.ch == 'K') { fixCurLine(); curline.crop(); continue; }
415 if (!key.alt && key.ctrl && !key.shift && key.ch == 'Y') { fixCurLine(); curline.clear(); continue; }
416 if (!key.alt && key.ctrl && !key.shift && key.ch == 'A') { curline.movePos(-curline.MaxLen); continue; }
417 if (!key.alt && key.ctrl && !key.shift && key.ch == 'E') { curline.movePos(curline.MaxLen); continue; }
418 continue;
420 switch (key.key) {
421 case TtyEvent.Key.Left:
422 if (!key.alt && !key.shift) {
423 if (key.ctrl) {
424 if (curline.pos > 0) {
425 if (curline[curline.pos-1] <= ' ') {
426 // move to word end
427 while (curline.pos > 0 && curline[curline.pos-1] <= ' ') curline.movePos(-1);
428 } else {
429 // move to word start
430 while (curline.pos > 0 && curline[curline.pos-1] > ' ') curline.movePos(-1);
433 } else {
434 curline.movePos(-1);
437 break;
438 case TtyEvent.Key.Right:
439 if (!key.alt && !key.shift) {
440 if (key.ctrl) {
441 if (curline.pos < curline.length) {
442 if (curline[curline.pos] <= ' ') {
443 // move to word start
444 while (curline.pos < curline.length && curline[curline.pos] <= ' ') curline.movePos(1);
445 } else {
446 // move to word end
447 while (curline.pos < curline.length && curline[curline.pos] > ' ') curline.movePos(1);
450 } else {
451 curline.movePos(1);
454 break;
455 case TtyEvent.Key.Home:
456 if (!key.ctrl && !key.alt && !key.shift) curline.movePos(-curline.MaxLen);
457 break;
458 case TtyEvent.Key.End:
459 if (!key.ctrl && !key.alt && !key.shift) curline.movePos(curline.MaxLen);
460 break;
461 case TtyEvent.Key.Enter:
462 fixCurLine();
463 wrt("\r\n");
464 return Result.Normal;
465 case TtyEvent.Key.Tab:
466 if (!key.ctrl && !key.alt && !key.shift) { fixCurLine(); autocomplete(); continue; }
467 break;
468 case TtyEvent.Key.Up:
469 if (!key.ctrl && !key.alt && !key.shift) {
470 if (history.length == 0) continue;
471 if (hpos == -1) {
472 // store current line so we can return to it
473 lastInput[0..curline.length] = curline[];
474 lastLen = curline.length;
475 hpos = 0;
476 } else if (hpos < history.length-1) {
477 ++hpos;
478 } else {
479 continue;
481 curline.set(history[hpos]);
482 curofs = 0;
484 break;
485 case TtyEvent.Key.Down:
486 if (!key.ctrl && !key.alt && !key.shift) {
487 if (history.length == 0) continue;
488 if (hpos == 0) {
489 // restore previous user line
490 hpos = -1;
491 curline.set(lastInput[0..lastLen]);
492 curofs = 0;
493 } else if (hpos > 0) {
494 --hpos;
495 curline.set(history[hpos]);
496 curofs = 0;
499 break;
500 case TtyEvent.Key.Backspace:
501 if (!key.ctrl && !key.alt && !key.shift) {
502 if (curline.length > 0 && curline.pos > 0) {
503 fixCurLine();
504 curline.backspace(1);
506 } else if (!key.ctrl && key.alt && !key.shift) {
507 delPrevWord();
509 break;
510 case TtyEvent.Key.Delete:
511 if (!key.ctrl && !key.alt && !key.shift) {
512 if (curline.pos < curline.length) {
513 fixCurLine();
514 curline.remove(1);
517 break;
518 case TtyEvent.Key.Char:
519 if (key.ch >= ' ' && key.ch < 127) {
520 if (curline.length < Line.MaxLen) {
521 fixCurLine();
522 curline.insert(cast(char)key.ch);
525 break;
526 default:
531 void autocomplete () {}
533 public:
534 static:
535 void clearOutput () { wrt("\r\x1b[K"); }
537 void wrt (const(char)[] str...) nothrow @nogc {
538 import core.sys.posix.unistd : write;
539 if (str.length) write(1, str.ptr, str.length);
542 void wrtuint (int n) nothrow @nogc {
543 char[32] buf = void;
544 uint pos = buf.length;
545 if (n < 0) n = 0;
546 do {
547 buf.ptr[--pos] = cast(char)(n%10+'0');
548 n /= 10;
549 } while (n != 0);
550 wrt(buf[pos..$]);
553 void beep () { wrt('\x07'); }
557 // ////////////////////////////////////////////////////////////////////////// //
558 version(editline_test) {
559 import iv.strex;
560 void main () {
561 auto el = new EditLine();
562 for (;;) {
563 import std.stdio;
564 auto res = el.readline();
565 writeln;
566 if (res != EditLine.Result.Normal) break;
567 writeln("[", el.get.quote, "]");
568 writeln(el.line.wordCount, " words in line");
569 foreach (const ref wp; el.line.words) writeln(" ", el.line[wp].quote);
570 el.pushCurrentToHistory();