sq3: show SQLite error messages on stderr by default
[iv.d.git] / egeditor / editor.d
blob88bd93664abb9d66976ce39666c8aba0417e4445
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.egeditor.editor /*is aliced*/;
18 //version = egeditor_scan_time;
19 //version = egeditor_scan_time_to_file;
20 // this is idiocity, i should change my habits
21 //version = egeditor_record_movement_undo;
23 import iv.alice;
24 import iv.strex;
25 import iv.utfutil;
26 import iv.vfs;
27 debug import iv.vfs.io;
29 version(egeditor_scan_time) import iv.pxclock;
31 version(egeditor_record_movement_undo) {
32 public enum EgEditorMovementUndo = true;
33 } else {
34 public enum EgEditorMovementUndo = false;
38 // ////////////////////////////////////////////////////////////////////////// //
39 /// this interface is used to measure text for pixel-sized editor
40 abstract class EgTextMeter {
41 int currofs; /// x offset for current char (i.e. the last char that was passed to `advance()` should be drawn with this offset)
42 int currwdt; /// current line width (including, the last char that was passed to `advance()`), preferably without trailing empty space between chars
43 int currheight; /// current text height; keep this in sync with the current state; `reset` should set it to "default text height"
45 /// this should reset text width iterator (and curr* fields); tabsize > 0: process tabs as... well... tabs ;-); tabsize < 0: calculating height
46 abstract void reset (int tabsize) nothrow;
48 /// advance text width iterator, fix all curr* fields
49 abstract void advance (dchar ch, in ref GapBuffer.HighState hs) nothrow;
51 /// finish text iterator; it should NOT reset curr* fields!
52 /// WARNING: EditorEngine tries to call this after each `reset()`, but user code may not
53 abstract void finish () nothrow;
57 // ////////////////////////////////////////////////////////////////////////// //
58 /// highlighter should be able to work line-by-line
59 class EditorHL {
60 protected:
61 GapBuffer gb; /// this will be set by EditorEngine on attaching
62 LineCache lc; /// this will be set by EditorEngine on attaching
64 public:
65 this () {} ///
67 /// return true if highlighting for this line was changed
68 abstract bool fixLine (int line);
70 /// mark line as "need rehighlighting" (and possibly other text too)
71 /// wasInsDel: some lines was inserted/deleted down the text
72 abstract void lineChanged (int line, bool wasInsDel);
76 // ////////////////////////////////////////////////////////////////////////// //
77 ///
78 public final class GapBuffer {
79 public:
80 static align(1) struct HighState {
81 align(1):
82 ubyte kwtype; // keyword number
83 ubyte kwidx; // index in keyword
84 @property pure nothrow @safe @nogc {
85 ushort u16 () const { pragma(inline, true); return cast(ushort)((kwidx<<8)|kwtype); }
86 short s16 () const { pragma(inline, true); return cast(short)((kwidx<<8)|kwtype); }
87 void u16 (ushort v) { pragma(inline, true); kwtype = v&0xff; kwidx = (v>>8)&0xff; }
88 void s16 (short v) { pragma(inline, true); kwtype = v&0xff; kwidx = (v>>8)&0xff; }
92 private:
93 HighState hidummy;
94 bool mSingleLine;
96 protected:
97 enum MinGapSize = 1024; // bytes in gap
98 enum GrowGran = 65536; // must be power of 2
99 enum MinGapSizeSmall = 64; // bytes in gap
100 enum GrowGranSmall = 0x100; // must be power of 2
102 static assert(GrowGran >= MinGapSize);
103 static assert(GrowGranSmall >= MinGapSizeSmall);
105 @property uint MGS () const pure nothrow @safe @nogc { pragma(inline, true); return (mSingleLine ? MinGapSizeSmall : MinGapSize); }
107 protected:
108 char* tbuf; // text buffer
109 HighState* hbuf; // highlight buffer
110 uint tbused; // not including gap
111 uint tbsize; // including gap
112 uint tbmax = 512*1024*1024+MinGapSize; // maximum buffer size
113 uint gapstart, gapend; // tbuf[gapstart..gapend]; gap cannot be empty
114 uint bufferChangeCounter; // will simply increase on each buffer change
116 static private bool xrealloc(T) (ref T* ptr, ref uint cursize, int newsize, uint gran) nothrow @trusted @nogc {
117 import core.stdc.stdlib : realloc;
118 assert(gran > 1);
119 uint nsz = ((newsize+gran-1)/gran)*gran;
120 assert(nsz >= newsize);
121 T* nb = cast(T*)realloc(ptr, nsz*T.sizeof);
122 if (nb is null) return false;
123 cursize = nsz;
124 ptr = nb;
125 return true;
128 final:
129 // initial alloc
130 void initTBuf (bool hadHBuf) nothrow @nogc {
131 import core.stdc.stdlib : free, malloc, realloc;
132 assert(tbuf is null);
133 assert(hbuf is null);
134 immutable uint nsz = (mSingleLine ? GrowGranSmall : GrowGran);
135 tbuf = cast(char*)malloc(nsz);
136 if (tbuf is null) assert(0, "out of memory for text buffers");
137 // allocate highlight buffer if necessary
138 if (hadHBuf) {
139 hbuf = cast(HighState*)malloc(nsz*hbuf[0].sizeof);
140 if (hbuf is null) assert(0, "out of memory for text buffers");
141 hbuf[0..nsz] = HighState.init;
143 tbused = 0;
144 tbsize = nsz;
145 gapstart = 0;
146 gapend = tbsize;
149 // ensure that we can place a text of size `size` in buffer, and will still have at least MGS bytes free
150 // may change `tbsize`, but will not touch `tbused`
151 bool growTBuf (uint size) nothrow @nogc {
152 if (size > tbmax) return false; // too big
153 immutable uint mingapsize = MGS; // desired gap buffer size
154 immutable uint unused = tbsize-tbused; // number of unused bytes in buffer
155 assert(tbused <= tbsize);
156 if (size <= tbused && unused >= mingapsize) return true; // nothing to do, we have enough room in buffer
157 // if the gap is bigger than the minimal gap size, check if we have enough extra bytes to avoid allocation
158 if (unused > mingapsize) {
159 immutable uint extra = unused-mingapsize; // extra bytes we can spend
160 immutable uint bgrow = size-tbused; // number of bytes we need
161 if (extra >= bgrow) return true; // yay, no need to realloc
163 // have to grow
164 immutable uint newsz = size+mingapsize;
165 immutable uint gran = (mSingleLine ? GrowGranSmall : GrowGran);
166 uint hbufsz = tbsize;
167 if (!xrealloc(tbuf, tbsize, newsz, gran)) return false;
168 // reallocate highlighting buffer only if we already have one
169 if (hbuf !is null) {
170 if (!xrealloc(hbuf, hbufsz, newsz, gran)) { tbsize = hbufsz; return false; } // HACK!
171 assert(tbsize == hbufsz);
173 assert(tbsize >= newsz);
174 return true;
177 protected:
178 uint pos2real (uint pos) const pure @safe nothrow @nogc {
179 pragma(inline, true);
180 return pos+(pos >= gapstart ? gapend-gapstart : 0);
183 public:
184 HighState defhs; /// default highlighting state for new text
186 public:
188 this (bool asingleline) nothrow @nogc {
189 mSingleLine = asingleline;
190 initTBuf(false); // don't allocate hbuf yet
194 ~this () nothrow @nogc {
195 import core.stdc.stdlib : free;
196 if (tbuf !is null) free(tbuf);
197 if (hbuf !is null) free(hbuf);
200 /// remove all text from buffer
201 /// WILL NOT call deletion hooks!
202 void clear () nothrow @nogc {
203 import core.stdc.stdlib : free;
204 immutable bool hadHBuf = (hbuf !is null);
205 if (tbuf !is null) { free(tbuf); tbuf = null; }
206 if (hadHBuf) { free(hbuf); hbuf = null; }
207 ++bufferChangeCounter;
208 initTBuf(hadHBuf);
211 @property bool hasHiBuffer () const pure nothrow @safe @nogc { pragma(inline, true); return (hbuf !is null); } ///
213 /// after calling this with `true`, `hasHiBuffer` may still be false if there is no memory for it
214 @property void hasHiBuffer (bool v) nothrow @trusted @nogc {
215 if (v != hasHiBuffer) {
216 if (v) {
217 // create highlighting buffer
218 import core.stdc.stdlib : malloc;
219 assert(hbuf is null);
220 assert(tbsize > 0);
221 hbuf = cast(HighState*)malloc(tbsize*hbuf[0].sizeof);
222 if (hbuf !is null) hbuf[0..tbsize] = HighState.init;
223 } else {
224 // remove highlighitng buffer
225 import core.stdc.stdlib : free;
226 assert(hbuf !is null);
227 free(hbuf);
228 hbuf = null;
233 /// "single line" mode, for line editors
234 bool singleline () const pure @safe nothrow @nogc { pragma(inline, true); return mSingleLine; }
236 /// size of text buffer without gap, in one-byte chars
237 @property int textsize () const pure @safe nothrow @nogc { pragma(inline, true); return tbused; }
239 @property char opIndex (uint pos) const pure @trusted nothrow @nogc { pragma(inline, true); return (pos < tbused ? tbuf[pos+(pos >= gapstart ? gapend-gapstart : 0)] : '\n'); } ///
240 @property ref HighState hi (uint pos) pure @trusted nothrow @nogc { pragma(inline, true); return (hbuf !is null && pos < tbused ? hbuf[pos+(pos >= gapstart ? gapend-gapstart : 0)] : (hidummy = hidummy.init)); } ///
242 @property dchar uniAt (uint pos) const @trusted nothrow @nogc {
243 immutable ts = tbused;
244 if (pos >= ts) return '\n';
245 Utf8DecoderFast udc;
246 while (pos < ts) {
247 if (udc.decodeSafe(cast(ubyte)tbuf[pos2real(pos++)])) return udc.codepoint;
249 return udc.codepoint;
252 @property dchar uniAtAndAdvance (ref int pos) const @trusted nothrow @nogc {
253 immutable ts = tbused;
254 if (pos < 0) pos = 0;
255 if (pos >= ts) return '\n';
256 Utf8DecoderFast udc;
257 while (pos < ts) {
258 if (udc.decodeSafe(cast(ubyte)tbuf[pos2real(pos++)])) return udc.codepoint;
260 return udc.codepoint;
263 /// return utf-8 character length at buffer position pos or -1 on error (or 1 on error if "always positive")
264 /// never returns zero
265 int utfuckLenAt(bool alwaysPositive=true) (int pos) const @trusted nothrow @nogc {
266 immutable ts = tbused;
267 if (pos < 0 || pos >= ts) {
268 static if (alwaysPositive) return 1; else return -1;
270 char ch = tbuf[pos2real(pos)];
271 if (ch < 128) return 1;
272 Utf8DecoderFast udc;
273 auto spos = pos;
274 while (pos < ts) {
275 ch = tbuf[pos2real(pos++)];
276 if (udc.decode(cast(ubyte)ch)) {
277 static if (alwaysPositive) {
278 return (udc.invalid ? 1 : pos-spos);
279 } else {
280 return (udc.invalid ? -1 : pos-spos);
284 static if (alwaysPositive) return 1; else return -1;
287 // ensure that the buffer has room for at least one char in the gap
288 // note that this may move gap
289 protected void ensureGap () pure @safe nothrow @nogc {
290 pragma(inline, true);
291 // if we have zero-sized gap, assume that it is at end; we always have a room for at least MinGapSize(Small) chars
292 if (gapstart >= gapend || gapstart >= tbused) {
293 assert(tbused <= tbsize);
294 gapstart = tbused;
295 gapend = tbsize;
296 assert(gapend-gapstart >= MGS);
300 /// put the gap *before* `pos`
301 void moveGapAtPos (int pos) @trusted nothrow @nogc {
302 import core.stdc.string : memmove;
303 immutable ts = tbused; // i will need it in several places
304 if (pos < 0) pos = 0;
305 if (pos > ts) pos = ts;
306 if (ts == 0) { gapstart = 0; gapend = tbsize; return; } // unlikely case, but...
307 ensureGap(); // we should have a gap
308 /* cases:
309 * pos is before gap: shift [pos..gapstart] to gapend-len, shift gap
310 * pos is after gap: shift [gapend..pos] to gapstart, shift gap
312 if (pos < gapstart) {
313 // pos is before gap
314 int len = gapstart-pos; // to shift
315 memmove(tbuf+gapend-len, tbuf+pos, len);
316 if (hbuf !is null) memmove(hbuf+gapend-len, hbuf+pos, len*hbuf[0].sizeof);
317 gapstart -= len;
318 gapend -= len;
319 } else if (pos > gapstart) {
320 // pos is after gap
321 int len = pos-gapstart;
322 memmove(tbuf+gapstart, tbuf+gapend, len);
323 if (hbuf !is null) memmove(hbuf+gapstart, hbuf+gapend, len*hbuf[0].sizeof);
324 gapstart += len;
325 gapend += len;
327 // if we moved gap to buffer end, grow it; `ensureGap()` will do it for us
328 ensureGap();
329 assert(gapstart == pos);
330 assert(gapstart < gapend);
331 assert(gapstart <= ts);
334 /// put the gap at the end of the text
335 void moveGapAtEnd () @trusted nothrow @nogc { moveGapAtPos(tbused); }
337 /// put text into buffer; will either put all the text, or nothing
338 /// returns success flag
339 bool append (const(char)[] str...) @trusted nothrow @nogc { return (put(tbused, str) >= 0); }
341 /// put text into buffer; will either put all the text, or nothing
342 /// returns new position or -1
343 int put (int pos, const(char)[] str...) @trusted nothrow @nogc {
344 import core.stdc.string : memcpy;
345 if (pos < 0) pos = 0;
346 bool atend = (pos >= tbused);
347 if (atend) pos = tbused;
348 if (str.length == 0) return pos;
349 if (tbmax-(tbsize-tbused) < str.length) return -1; // no room
350 if (!growTBuf(tbused+cast(uint)str.length)) return -1; // memory allocation failed
351 //TODO: this can be made faster, but meh...
352 immutable slen = cast(uint)str.length;
353 if (atend || gapend-gapstart < slen) moveGapAtEnd(); // this will grow the gap, so it will take all available room
354 if (!atend) moveGapAtPos(pos); // condition is used for tiny speedup
355 assert(gapend-gapstart >= slen);
356 memcpy(tbuf+gapstart, str.ptr, str.length);
357 if (hbuf !is null) hbuf[gapstart..gapstart+str.length] = defhs;
358 gapstart += slen;
359 tbused += slen;
360 pos += slen;
361 ensureGap();
362 assert(tbsize-tbused >= MGS);
363 ++bufferChangeCounter;
364 return pos;
367 /// remove count bytes from the current position; will either remove all of 'em, or nothing
368 /// returns success flag
369 bool remove (int pos, int count) @trusted nothrow @nogc {
370 import core.stdc.string : memmove;
371 if (count < 0) return false;
372 if (count == 0) return true;
373 immutable ts = tbused; // cache current text size
374 if (pos < 0) pos = 0;
375 if (pos > ts) pos = ts;
376 if (ts-pos < count) return false; // not enough text here
377 assert(gapstart < gapend);
378 ++bufferChangeCounter; // buffer will definitely be changed
379 for (;;) {
380 // at the start of the gap: i can just increase gap
381 if (pos == gapstart) {
382 gapend += count;
383 tbused -= count;
384 return true;
386 // removing text just before gap: increase gap (backspace does this)
387 if (pos+count == gapstart) {
388 gapstart -= count;
389 tbused -= count;
390 assert(gapstart == pos);
391 return true;
393 // both variants failed; move gap at `pos` and try again
394 moveGapAtPos(pos);
398 /// count how much eols we have in this range
399 int countEolsInRange (int pos, int count) const @trusted nothrow @nogc {
400 import core.stdc.string : memchr;
401 if (count < 1 || pos <= -count || pos >= tbused) return 0;
402 if (pos+count > tbused) count = tbused-pos;
403 int res = 0;
404 while (count > 0) {
405 int npos = fastFindCharIn(pos, count, '\n')+1;
406 if (npos <= 0) break;
407 ++res;
408 count -= (npos-pos);
409 pos = npos;
411 return res;
414 /// using `memchr`, jumps over gap; never moves after `tbused`
415 public uint fastSkipEol (int pos) const @trusted nothrow @nogc {
416 import core.stdc.string : memchr;
417 immutable ts = tbused;
418 if (ts == 0 || pos >= ts) return ts;
419 if (pos < 0) pos = 0;
420 // check text before gap
421 if (pos < gapstart) {
422 auto fp = cast(char*)memchr(tbuf+pos, '\n', gapstart-pos);
423 if (fp !is null) return cast(int)(fp-tbuf)+1;
424 pos = gapstart; // new starting position
426 assert(pos >= gapstart);
427 // check after gap and to text end
428 int left = ts-pos;
429 if (left > 0) {
430 auto stx = tbuf+gapend+(pos-gapstart);
431 assert(cast(usize)(tbuf+tbsize-stx) >= left);
432 auto fp = cast(char*)memchr(stx, '\n', left);
433 if (fp !is null) return pos+cast(int)(fp-stx)+1;
435 return ts;
438 /// using `memchr`, jumps over gap; returns `tbused` if not found
439 public uint fastFindChar (int pos, char ch) const @trusted nothrow @nogc {
440 int res = fastFindCharIn(pos, tbused, ch);
441 return (res >= 0 ? res : tbused);
444 /// use `memchr`, jumps over gap; returns -1 if not found
445 public int fastFindCharIn (int pos, int len, char ch) const @trusted nothrow @nogc {
446 import core.stdc.string : memchr;
447 immutable ts = tbused;
448 if (len < 1) return -1;
449 if (ts == 0 || pos >= ts) return -1;
450 if (pos < 0) {
451 if (pos <= -len) return -1;
452 len += pos;
453 pos = 0;
455 if (tbused-pos < len) len = tbused-pos;
456 assert(len > 0);
457 int left;
458 // check text before gap
459 if (pos < gapstart) {
460 left = gapstart-pos;
461 if (left > len) left = len;
462 auto fp = cast(char*)memchr(tbuf+pos, ch, left);
463 if (fp !is null) return cast(int)(fp-tbuf);
464 if ((len -= left) == 0) return -1;
465 pos = gapstart; // new starting position
467 assert(pos >= gapstart);
468 // check after gap and to text end
469 left = ts-pos;
470 if (left > len) left = len;
471 if (left > 0) {
472 auto stx = tbuf+gapend+(pos-gapstart);
473 assert(cast(usize)(tbuf+tbsize-stx) >= left);
474 auto fp = cast(char*)memchr(stx, ch, left);
475 if (fp !is null) return pos+cast(int)(fp-stx);
477 return -1;
480 /// bufparts range
481 /// this is hack for regexp searchers
482 /// do not store returned slice anywhere for a long time!
483 /// slice *will* be invalidated on next gap buffer operation!
484 public auto bufparts (int pos) nothrow @nogc {
485 static struct Range {
486 nothrow @nogc:
487 GapBuffer gb;
488 bool aftergap; // fr is "aftergap"?
489 const(char)[] fr;
490 private this (GapBuffer agb, int pos) {
491 gb = agb;
492 auto ts = agb.tbused;
493 if (ts == 0 || pos >= ts) { gb = null; return; }
494 if (pos < 0) pos = 0;
495 if (pos < agb.gapstart) {
496 fr = agb.tbuf[pos..agb.gapstart];
497 } else {
498 int left = ts-pos;
499 if (left < 1) { gb = null; return; }
500 pos -= agb.gapstart;
501 fr = agb.tbuf[agb.gapend+pos..agb.gapend+pos+left];
502 aftergap = true;
505 @property bool empty () pure const @safe { pragma(inline, true); return (gb is null); }
506 @property const(char)[] front () pure @safe { pragma(inline, true); return fr; }
507 void popFront () {
508 if (aftergap) gb = null;
509 if (gb is null) { fr = null; return; }
510 int left = gb.textsize-gb.gapstart;
511 if (left < 1) { gb = null; fr = null; return; }
512 fr = gb.tbuf[gb.gapend..gb.gapend+left];
513 aftergap = true;
516 return Range(this, pos);
519 /// this calls dg with continuous buffer parts, so you can write 'em to a file, for example
520 public final void forEachBufPart (int pos, int len, scope void delegate (const(char)[] buf) dg) {
521 if (dg is null) return;
522 immutable ts = tbused;
523 if (len < 1) return;
524 if (ts == 0 || pos >= ts) return;
525 if (pos < 0) {
526 if (pos <= -len) return;
527 len += pos;
528 pos = 0;
530 assert(len > 0);
531 int left;
532 // check text before gap
533 if (pos < gapstart) {
534 left = gapstart-pos;
535 if (left > len) left = len;
536 assert(left > 0);
537 dg(tbuf[pos..pos+left]);
538 if ((len -= left) == 0) return; // nothing more to do
539 pos = gapstart; // new starting position
541 assert(pos >= gapstart);
542 // check after gap and to text end
543 left = ts-pos;
544 if (left > len) left = len;
545 if (left > 0) {
546 auto stx = tbuf+gapend+(pos-gapstart);
547 assert(cast(usize)(tbuf+tbsize-stx) >= left);
548 dg(stx[0..left]);
552 public:
554 static bool hasEols (const(char)[] str) pure nothrow @trusted @nogc {
555 import core.stdc.string : memchr;
556 uint left = cast(uint)str.length;
557 return (left > 0 && memchr(str.ptr, '\n', left) !is null);
560 /// index or -1
561 static int findEol (const(char)[] str) pure nothrow @trusted @nogc {
562 if (str.length > int.max) assert(0, "string too long");
563 int left = cast(int)str.length;
564 if (left > 0) {
565 import core.stdc.string : memchr;
566 auto dp = cast(const(char)*)memchr(str.ptr, '\n', left);
567 if (dp !is null) return cast(int)(dp-str.ptr);
569 return -1;
572 /// count number of '\n' chars in string
573 static int countEols (const(char)[] str) pure nothrow @trusted @nogc {
574 import core.stdc.string : memchr;
575 if (str.length > int.max) assert(0, "string too long");
576 int count = 0;
577 uint left = cast(uint)str.length;
578 auto dsp = str.ptr;
579 while (left > 0) {
580 auto ep = cast(const(char)*)memchr(dsp, '\n', left);
581 if (ep is null) break;
582 ++count;
583 ++ep;
584 left -= cast(uint)(ep-dsp);
585 dsp = ep;
587 return count;
592 // ////////////////////////////////////////////////////////////////////////// //
593 // Self-Healing Line Cache (utm) implementation
594 //TODO(?): don't do full cache repairing
595 private final class LineCache {
596 private:
597 // line offset/height cache item
598 // to be safe, locache[mLineCount] is always valid and holds gb.textsize
599 static align(1) struct LOCItem {
600 align(1):
601 uint ofs;
602 private uint mheight; // 0: unknown; line height; high bit is reserved for "viswrap" flag
603 pure nothrow @safe @nogc:
604 @property bool validHeight () const { pragma(inline, true); return ((mheight&0x7fff_ffffU) != 0); }
605 @property void resetHeight () { pragma(inline, true); mheight &= 0x8000_0000U; } // doesn't reset "viswrap" flag
606 @property uint height () const { pragma(inline, true); return (mheight&0x7fff_ffffU); }
607 @property void height (uint v) { pragma(inline, true); assert(v <= 0x7fff_ffffU); mheight = (mheight&0x8000_0000)|v; }
608 @property bool viswrap () const { pragma(inline, true); return ((mheight&0x8000_0000U) != 0); }
609 @property viswrap (bool v) { pragma(inline, true); if (v) mheight |= 0x8000_0000U; else mheight &= 0x7fff_ffffU; }
610 @property void resetHeightAndWrap () { pragma(inline, true); mheight = 0; }
611 @property void resetHeightAndSetWrap () { pragma(inline, true); mheight = 0x8000_0000U; }
612 void initWithPos (uint pos) { pragma(inline, true); ofs = pos; mheight = 0; }
615 private:
616 GapBuffer gb;
617 LOCItem* locache; // line info cache
618 uint locsize; // number of allocated items in locache
619 uint mLineCount; // total number of lines (and valid items in cache too)
620 EgTextMeter textMeter; // null: monospaced font
621 int mLineHeight = 1; // line height, in pixels/cells; <0: variable; 0: invalid state
622 dchar delegate (char ch) nothrow recode1byte; // not null: delegate to recode from 1bt to unishit
623 int mWordWrapWidth = 0; // >0: "visual wrap"; either in pixels (if textMeter is set) or in cells
625 public:
626 // utfuck-8 and visual tabs support
627 // WARNING! this will SIGNIFICANTLY slow down coordinate calculations!
628 bool utfuck = false; /// should x coordinate calculation assume that the text is in UTF-8?
629 bool visualtabs = false; /// should x coordinate calculation assume that tabs are not one-char width?
630 ubyte tabsize = 2; /// tab size, in spaces
632 final: // just in case the compiler won't notice "final class"
633 private:
634 void initLC () nothrow @nogc {
635 import core.stdc.stdlib : realloc;
636 // allocate initial line cache
637 uint ICS = (gb.mSingleLine ? 2 : 1024);
638 locache = cast(typeof(locache[0])*)realloc(locache, ICS*locache[0].sizeof);
639 if (locache is null) assert(0, "out of memory for line cache");
640 locache[0..ICS] = LOCItem.init;
641 locache[1].ofs = gb.textsize; // just in case
642 locsize = ICS;
643 mLineCount = 1; // we always have at least one line, even if it is empty
646 // `total` is new number of entries in cache; actual number will be greater by one
647 bool growLineCache (uint total) nothrow @nogc {
648 if (total >= int.max/8) assert(0, "egeditor: wtf?!");
649 ++total; // for last item
650 if (locsize < total) {
651 // have to allocate more
652 if (!GapBuffer.xrealloc(locache, locsize, total, (locsize < 4096 ? 0x400 : 0x1000))) return false;
654 return true;
657 int calcLineHeight (int lidx) nothrow {
658 assert(lidx >= 0 && lidx < mLineCount);
659 int ls = locache[lidx].ofs;
660 int le = locache[lidx+1].ofs;
661 if (locache[lidx].viswrap) ++le;
662 textMeter.reset(-1); // nobody cares about tab widths here
663 scope(exit) textMeter.finish();
664 int maxh = textMeter.currheight;
665 if (maxh < 1) maxh = 1;
666 auto tbufcopy = gb.tbuf;
667 auto hbufcopy = gb.hbuf;
668 gb.hidummy = GapBuffer.HighState.init; // just in case
669 if (utfuck) {
670 Utf8DecoderFast udc;
671 GapBuffer.HighState* hs = (hbufcopy !is null ? hbufcopy+gb.pos2real(ls) : &gb.hidummy);
672 while (ls < le) {
673 char ch = tbufcopy[gb.pos2real(ls++)];
674 if (udc.decodeSafe(cast(ubyte)ch)) {
675 immutable dchar dch = udc.codepoint;
676 textMeter.advance(dch, *hs);
678 if (textMeter.currheight > maxh) maxh = textMeter.currheight;
679 if (ls < le && hbufcopy !is null) hs = hbufcopy+gb.pos2real(ls);
681 } else {
682 auto rc1b = recode1byte;
683 while (ls < le) {
684 immutable uint rpos = gb.pos2real(ls++);
685 dchar dch = (rc1b !is null ? rc1b(tbufcopy[rpos]) : cast(dchar)tbufcopy[rpos]);
686 GapBuffer.HighState* hs = (hbufcopy !is null ? hbufcopy+rpos : &gb.hidummy);
687 textMeter.advance(dch, *hs);
688 if (textMeter.currheight > maxh) maxh = textMeter.currheight;
691 //{ import core.stdc.stdio; printf("line #%d height is %d\n", lidx, maxh); }
692 return maxh;
695 // -1: not found
696 int findLineCacheIndex (uint pos) const nothrow @nogc {
697 int lcount = mLineCount;
698 if (lcount <= 0) return -1;
699 if (pos >= gb.tbused) return lcount-1;
700 if (lcount == 1) return (pos < locache[1].ofs ? 0 : -1);
701 if (pos < locache[lcount].ofs) {
702 // yay! use binary search to find the line
703 int bot = 0, i = lcount-1;
704 while (bot != i) {
705 int mid = i-(i-bot)/2;
706 //!assert(mid >= 0 && mid < locused);
707 immutable ls = locache[mid].ofs;
708 immutable le = locache[mid+1].ofs;
709 if (pos >= ls && pos < le) return mid; // i found her!
710 if (pos < ls) i = mid-1; else bot = mid;
712 return i;
714 return -1;
717 // get range for current wrapped line: [ltop..lbot)
718 void wwGetTopBot (int lidx, int* ltop, int* lbot) const nothrow @trusted @nogc {
719 if (lidx < 0) lidx = 0;
720 if (lidx >= mLineCount) { *ltop = *lbot = mLineCount; return; }
721 *ltop = *lbot = lidx;
722 if (mWordWrapWidth <= 0) { *lbot += 1; return; }
723 immutable bool curIsMiddle = locache[lidx].viswrap;
724 // find first (top) line move up to previous unwrapped, and then one down
725 if (lidx > 0) while (lidx > 0 && locache[lidx-1].viswrap) --lidx;
726 *ltop = lidx;
727 // find last (bottom) line (if current is wrapped, move down to first unwrapped; then one down anyway)
728 lidx = *lbot;
729 if (curIsMiddle) while (lidx < mLineCount && locache[lidx].viswrap) ++lidx;
730 if (++lidx > mLineCount) lidx = mLineCount; // just in case
731 *lbot = lidx;
732 assert(*ltop < *lbot);
735 int collapseWrappedLine (int lidx) nothrow @nogc {
736 import core.stdc.string : memmove;
737 if (mWordWrapWidth <= 0 || gb.mSingleLine || lidx < 0 || lidx >= mLineCount) return lidx;
738 if (!locache[lidx].viswrap) return lidx; // early exit
739 int ltop, lbot;
740 wwGetTopBot(lidx, &ltop, &lbot);
741 immutable int tokill = lbot-ltop-1;
742 version(none) { import core.stdc.stdio; printf("collapsing: lidx=%d; ltop=%d; lbot=%d; tokill=%d; left=%d\n", lidx, ltop, lbot, tokill, mLineCount-lbot); }
743 if (tokill <= 0) return lidx; // nothing to do
744 // remove cache items for wrapped lines
745 if (lbot <= mLineCount) memmove(locache+ltop+1, locache+lbot, (mLineCount-lbot+1)*locache[0].sizeof);
746 lbot -= tokill;
747 mLineCount -= tokill;
748 // and fix wrapping flag
749 locache[ltop].resetHeightAndWrap();
750 return ltop;
753 // do word wrapping; line cache should be calculated and repaired for the given line
754 // (and down, if it was already wrapped)
755 // returns next line index to possible wrap
756 //TODO: "unwrap" line if word wrapping is not set?
757 //TODO: make it faster
758 //TODO: unicode blanks?
759 int doWordWrapping (int lidx) nothrow {
760 import core.stdc.string : memmove;
762 static bool isBlank (char ch) pure nothrow @safe @nogc { pragma(inline, true); return (ch == '\t' || ch == ' '); }
764 immutable int www = mWordWrapWidth;
765 if (www <= 0 || gb.mSingleLine) return mLineCount;
766 if (lidx < 0) lidx = 0;
767 if (lidx >= mLineCount) return mLineCount;
768 // find first and last line, if it was already wrapped
769 int ltop, lbot;
770 wwGetTopBot(lidx, &ltop, &lbot);
771 // now the hard part
772 if (textMeter) textMeter.reset(visualtabs ? tabsize : 0);
773 scope(exit) textMeter.finish();
774 version(none) { import core.stdc.stdio; printf("lidx=%d; ltop=%d; lbot=%d, linecount=%d\n", lidx, ltop, lbot, mLineCount); }
775 // we have line range here; go, ninja, go
776 lidx = ltop;
777 int cpos = locache[lidx].ofs;
778 int cwdt = 0;
779 int lastWordStartPos = 0;
780 immutable bool utfuckmode = utfuck;
781 immutable int tabsz = (visualtabs ? tabsize : 0);
782 auto tm = textMeter;
783 while (gb[cpos] != '\n') {
784 // go until cwdt allows us; but we have to have at least one char
785 int nwdt; // "new" width with the current char
786 int lpos = cpos; // "last position"
787 if (tm is null) {
788 if (tabsz > 0 && gb[cpos] == '\t') {
789 // skip tab
790 nwdt = ((cwdt+tabsz)/tabsz)*tabsz;
791 } else {
792 nwdt = cwdt+1;
793 if (utfuckmode) cpos += gb.utfuckLenAt!true(cpos); else ++cpos;
795 } else {
796 dchar dch = (utfuckmode ? gb.uniAtAndAdvance(cpos) : recode1byte is null ? cast(dchar)gb[cpos++] : recode1byte(gb[cpos++]));
797 tm.advance(dch, gb.hi(lpos));
798 nwdt = tm.currwdt;
800 //{ import core.stdc.stdio; printf(" lidx=%d; lineofs=%d; linelen=%d; cwdt=%d; nwdt=%d; maxwdt=%d; lpos=%d; cpos=%d\n", lidx, locache[lidx].ofs, locache[lidx].len, cwdt, nwdt, www, lpos, cpos); }
801 // if we have at least one char in line, check if we should wrap here
802 if (lpos > locache[lidx].ofs) {
803 if (nwdt > www) {
804 // should wrap here
805 if (tm !is null) {
806 tm.finish();
807 tm.reset(visualtabs ? tabsize : 0);
809 // if we have at least one word, wrap on it
810 if (lastWordStartPos > 0) {
811 cpos = lastWordStartPos;
812 lastWordStartPos = 0;
813 } else {
814 cpos = lpos;
816 locache[lidx].resetHeightAndSetWrap();
817 ++lidx;
818 if (lidx == lbot) {
819 // insert new cache record
820 growLineCache(mLineCount+1);
821 if (lidx <= mLineCount) memmove(locache+lidx+1, locache+lidx, (mLineCount-lidx+1)*locache[0].sizeof);
822 ++mLineCount;
823 ++lbot;
825 // setup next line
826 locache[lidx] = LOCItem.init; // reset all, including height and wrapping
827 locache[lidx].ofs = cpos;
828 cwdt = 0;
829 continue;
830 } else {
831 // check for word boundary
832 if (isBlank(gb[lpos]) && !isBlank(gb[cpos])) lastWordStartPos = cpos;
835 // go on
836 cwdt = nwdt;
838 locache[lidx].resetHeightAndWrap();
839 // remove unused cache items
840 version(none) { import core.stdc.stdio; printf(" 00: lidx=%d; ltop=%d; lbot=%d, linecount=%d\n", lidx, ltop, lbot, mLineCount); }
841 assert(lidx < lbot);
842 //TODO: make it faster
843 ++lidx; // to kill: [lidx..lbot)
844 immutable int tokill = lbot-lidx;
845 if (tokill > 0) {
846 version(none) { import core.stdc.stdio; printf(" xx: lidx=%d; ltop=%d; lbot=%d, linecount=%d; tokill=%d\n", lidx, ltop, lbot, mLineCount, tokill); }
847 if (lbot <= mLineCount) memmove(locache+lidx, locache+lbot, (mLineCount-lbot+1)*locache[0].sizeof);
848 lbot -= tokill;
849 mLineCount -= tokill;
851 version(none) { import core.stdc.stdio; printf(" 01: lidx=%d; ltop=%d; lbot=%d, linecount=%d\n", lidx, ltop, lbot, mLineCount); }
852 assert(lidx == lbot);
853 assert(mLineCount > 0);
854 assert(locache[lbot].ofs == cpos+(cpos < gb.textsize ? 1 : 0));
855 return lbot;
858 public:
859 this (GapBuffer agb) nothrow @nogc {
860 assert(agb !is null);
861 gb = agb;
862 initLC();
865 ~this () nothrow @nogc {
866 if (locache !is null) {
867 import core.stdc.stdlib : free;
868 free(locache);
872 public:
873 /// there is always at least one line, so `linecount` is never zero
874 @property int linecount () const pure nothrow @safe @nogc { pragma(inline, true); return mLineCount; }
876 void clear () nothrow @nogc {
877 import core.stdc.stdlib : free;
878 gb.clear();
879 // free old buffers
880 if (locache !is null) { free(locache); locache = null; }
881 // allocate new buffer
882 initLC();
885 /** load file like this:
886 * if (!lc.resizeBuffer(filesize)) throw new Exception("memory?");
887 * scope(failure) lc.clear();
888 * fl.rawReadExact(lc.getBufferPtr[]);
889 * if (!lc.rebuild()) throw new Exception("memory?");
892 /// allocate text buffer for the text of the given size
893 bool resizeBuffer (uint newsize) nothrow @nogc {
894 if (newsize > gb.tbmax) return false;
895 clear();
896 //{ import core.stdc.stdio; printf("resizing buffer to %u bytes\n", newsize); }
897 if (!gb.growTBuf(newsize)) return false;
898 gb.tbused = gb.gapstart = newsize;
899 gb.gapend = gb.tbsize;
900 gb.ensureGap();
901 return true;
904 /// get continuous buffer pointer, so we can read the whole file into it
905 char[] getBufferPtr () nothrow @nogc {
906 gb.moveGapAtEnd();
907 return gb.tbuf[0..gb.textsize];
910 /// count lines, fill line cache, do word wrapping
911 bool rebuild () nothrow {
912 import core.stdc.string : memset;
913 //gb.moveGapAtEnd(); // just in case
914 immutable ts = gb.textsize;
915 const(char)* tb = gb.tbuf;
916 if (gb.mSingleLine) {
917 // easy
918 growLineCache(1);
919 assert(locsize > 0);
920 mLineCount = 1;
921 locache[0..2] = LOCItem.init;
922 locache[1].ofs = ts;
923 return true;
925 version(egeditor_scan_time) auto stt = clockMilli();
926 version(none) {
927 // less memory fragmentation
928 int lcount = gb.countEolsInRange(0, ts)+1; // total number of lines
929 //{ import core.stdc.stdio; printf("loaded %u bytes; %d lines found\n", gb.textsize, lcount); }
930 if (!growLineCache(lcount)) return false;
931 assert(lcount+1 <= locsize);
932 //locache[0..lcount+1] = LOCItem.init; // reset all lines
933 memset(locache, 0, (lcount+1)*locache[0].sizeof); // reset all lines (help compiler a little)
934 uint pos = 0;
935 LOCItem* lcp = locache; // help compiler a little
936 foreach (immutable uint lidx; 0..lcount) {
937 lcp.initWithPos(pos);
938 ++lcp;
939 pos = gb.fastSkipEol(pos);
941 // last line
942 assert(lcp is locache+lcount);
943 lcp.ofs = gb.textsize;
944 } else {
945 // faster scanning
946 if (gb.textsize == 0) {
947 // no text
948 if (!growLineCache(1)) return false; // should have at least one
949 assert(locsize >= 2);
950 locache[0].initWithPos(0);
951 locache[1].initWithPos(0);
952 mLineCount = 1;
953 } else {
954 int lcount = 0; // total number of lines
955 //{ import core.stdc.stdio; printf("loaded %u bytes; %d lines found\n", gb.textsize, lcount); }
956 if (!growLineCache(1)) return false; // should have at least one
957 uint pos = 0;
958 while (pos < ts) {
959 if (lcount+1 >= locsize) { if (!growLineCache(lcount+1)) return false; }
960 locache[lcount++].initWithPos(pos);
961 pos = gb.fastSkipEol(pos);
963 assert(lcount > 0);
964 // hack for last empty line, if it ends with '\n'
965 if (ts && gb[ts-1] == '\n') {
966 if (lcount+1 >= locsize) { if (!growLineCache(lcount+1)) return false; }
967 locache[lcount++].initWithPos(ts);
969 // last line
970 assert(pos == ts);
971 assert(lcount < locsize);
972 locache[lcount].initWithPos(pos);
973 mLineCount = lcount;
976 version(egeditor_scan_time) {
977 import core.stdc.stdio;
978 auto et = clockMilli()-stt;
979 version(egeditor_scan_time_to_file) {
980 if (auto fo = fopen("ztime.log", "a")) {
981 scope(exit) fo.fclose();
982 fo.fprintf("%u lines (%u bytes) scanned in %u milliseconds\n", mLineCount, gb.textsize, cast(uint)et);
984 } else {
985 printf("%u lines (%u bytes) scanned in %u milliseconds\n", mLineCount, gb.textsize, cast(uint)et);
987 stt = clockMilli(); // for wrapping
989 if (mWordWrapWidth > 0) {
990 int lidx = 0;
991 while (lidx < mLineCount) lidx = doWordWrapping(lidx);
992 version(egeditor_scan_time) {
993 import core.stdc.stdio;
994 et = clockMilli()-stt;
995 version(egeditor_scan_time_to_file) {
996 if (auto fo = fopen("ztime.log", "a")) {
997 scope(exit) fo.fclose();
998 fo.fprintf(" %u lines wrapped in %u milliseconds\n", mLineCount, cast(uint)et);
1000 } else {
1001 printf(" %u lines wrapped in %u milliseconds\n", mLineCount, cast(uint)et);
1005 return true;
1008 /// put text into buffer; will either put all the text, or nothing
1009 /// returns success flag
1010 bool append (const(char)[] str...) nothrow { return (put(gb.textsize, str) >= 0); }
1012 /// put text into buffer; will either put all the text, or nothing
1013 /// returns new position or -1
1014 int put (int pos, const(char)[] str...) nothrow {
1015 if (pos < 0) pos = 0;
1016 bool atend = (pos >= gb.textsize);
1017 if (str.length == 0) return pos;
1018 if (atend) pos = gb.textsize;
1019 auto ppos = gb.put(pos, str);
1020 if (ppos < 0) return ppos;
1021 // heal line cache for single-line case
1022 if (gb.mSingleLine) {
1023 assert(mLineCount == 1);
1024 assert(locsize > 1);
1025 assert(locache[0].ofs == 0);
1026 locache[1].ofs = gb.textsize;
1027 locache[0].resetHeightAndWrap();
1028 locache[1].resetHeightAndWrap();
1029 } else {
1030 assert(ppos > pos);
1031 int newlines = GapBuffer.countEols(str);
1032 immutable insertedLines = newlines;
1033 auto lidx = findLineCacheIndex(pos);
1034 immutable int ldelta = ppos-pos;
1035 assert((!atend && lidx >= 0) || (atend && (lidx < 0 || lidx == mLineCount-1)));
1036 if (atend) lidx = mLineCount-1;
1037 int wraplidx = lidx;
1038 if (newlines == 0) {
1039 // no lines was inserted, just repair the length
1040 // no need to collapse wrapped line here, 'cause `doWordWrapping()` will rewrap it anyway
1041 locache[lidx++].resetHeight();
1042 } else {
1043 import core.stdc.string : memmove;
1044 //FIXME: make this faster for wrapped lines
1045 wraplidx = lidx = collapseWrappedLine(wraplidx);
1046 // we will start repairing from the last good line
1047 pos = locache[lidx].ofs;
1048 // inserted some new lines, make room for 'em
1049 growLineCache(mLineCount+newlines);
1050 if (lidx <= mLineCount) memmove(locache+lidx+newlines, locache+lidx, (mLineCount-lidx+1)*locache[0].sizeof);
1051 mLineCount += newlines;
1052 // no need to clear inserted lines, we'll overwrite em
1053 // recalc offsets and lengthes
1054 while (newlines-- >= 0) {
1055 locache[lidx].ofs = pos;
1056 locache[lidx++].resetHeightAndWrap();
1057 pos = gb.fastSkipEol(pos);
1060 // repair line cache (offsets) -- for now; switch to "repair on demand" later?
1061 if (lidx <= mLineCount) foreach (ref lc; locache[lidx..mLineCount+1]) lc.ofs += ldelta;
1062 if (mWordWrapWidth > 0) {
1063 foreach (immutable c; 0..insertedLines+1) wraplidx = doWordWrapping(wraplidx);
1066 return ppos;
1069 /// remove count bytes from the current position; will either remove all of 'em, or nothing
1070 /// returns success flag
1071 bool remove (int pos, int count) nothrow {
1072 if (gb.mSingleLine) {
1073 // easy
1074 if (!gb.remove(pos, count)) return false;
1075 assert(mLineCount == 1);
1076 assert(locsize > 1);
1077 assert(locache[0].ofs == 0);
1078 locache[1].ofs = gb.textsize;
1079 locache[0].resetHeightAndWrap();
1080 locache[1].resetHeightAndWrap();
1081 } else {
1082 // hard
1083 import core.stdc.string : memmove;
1084 if (count < 0) return false;
1085 if (count == 0) return true;
1086 if (pos < 0) pos = 0;
1087 if (pos > gb.textsize) pos = gb.textsize;
1088 if (gb.textsize-pos < count) return false; // not enough text here
1089 auto lidx = findLineCacheIndex(pos);
1090 assert(lidx >= 0);
1091 int newlines = gb.countEolsInRange(pos, count);
1092 if (!gb.remove(pos, count)) return false;
1093 // repair line cache
1094 if (newlines == 0) {
1095 // no need to collapse wrapped line here, 'cause `doWordWrapping()` will rewrap it anyway
1096 locache[lidx].resetHeight();
1097 } else {
1098 import core.stdc.string : memmove;
1099 if (mWordWrapWidth > 0) {
1100 // collapse wordwrapped line into one, it is easier this way; it is safe to collapse lines in modified text
1101 //FIXME: make this faster for wrapped lines
1102 lidx = collapseWrappedLine(lidx);
1103 // collapse deleted lines too, so we can remove 'em as if they were normal ones
1104 foreach (immutable c; 1..newlines+1) collapseWrappedLine(lidx+c);
1106 // remove unused lines
1107 if (lidx+1 <= mLineCount) memmove(locache+lidx+1, locache+lidx+1+newlines, (mLineCount-lidx)*locache[0].sizeof);
1108 mLineCount -= newlines;
1109 // fix current line
1110 locache[lidx].resetHeightAndWrap();
1112 if (lidx+1 <= mLineCount) foreach (ref lc; locache[lidx+1..mLineCount+1]) lc.ofs -= count;
1113 if (mWordWrapWidth > 0) {
1114 lidx = doWordWrapping(lidx);
1115 // and next one, 'cause it was modified by collapser
1116 doWordWrapping(lidx);
1119 return true;
1122 int lineHeightPixels (int lidx, bool forceRecalc=false) nothrow {
1123 int h;
1124 assert(textMeter !is null);
1125 if (lidx < 0 || mLineCount == 0 || lidx >= mLineCount) {
1126 textMeter.reset(0);
1127 h = (textMeter.currheight > 0 ? textMeter.currheight : 1);
1128 textMeter.finish();
1129 } else {
1130 if (forceRecalc || !locache[lidx].validHeight) locache[lidx].height = calcLineHeight(lidx);
1131 h = locache[lidx].height;
1133 return h;
1136 bool isLastWrappedLine (int lidx) nothrow @nogc { pragma(inline, true); return (lidx >= 0 && lidx < mLineCount ? !locache[lidx].viswrap : true); }
1137 bool isWrappedLine (int lidx) nothrow @nogc { pragma(inline, true); return (lidx >= 0 && lidx < mLineCount ? locache[lidx].viswrap : false); }
1139 /// get number of *symbols* to line end (this is not always equal to number of bytes for utfuck)
1140 int syms2eol (int pos) nothrow {
1141 immutable ts = gb.textsize;
1142 if (pos < 0) pos = 0;
1143 if (pos >= ts) return 0;
1144 int epos = line2pos(pos2line(pos)+1);
1145 if (!utfuck) return epos-pos; // fast path
1146 // slow path
1147 int count = 0;
1148 while (pos < epos) {
1149 pos += gb.utfuckLenAt!true(pos);
1150 ++count;
1152 return count;
1155 /// get line for the given position
1156 int pos2line (int pos) nothrow {
1157 immutable ts = gb.textsize;
1158 if (pos < 0) return 0;
1159 if (pos == 0 || ts == 0) return 0;
1160 if (pos >= ts) return mLineCount-1; // end of text: no need to update line offset cache
1161 if (mLineCount == 1) return 0;
1162 int lcidx = findLineCacheIndex(pos);
1163 assert(lcidx >= 0 && lcidx < mLineCount);
1164 return lcidx;
1167 /// get position (starting) for the given line
1168 /// it will be 0 for negative lines, and `textsize` for positive out of bounds lines
1169 int line2pos (int lidx) nothrow {
1170 if (lidx < 0 || gb.textsize == 0) return 0;
1171 if (lidx > mLineCount-1) return gb.textsize;
1172 if (mLineCount == 1) {
1173 assert(lidx == 0);
1174 return 0;
1176 return locache[lidx].ofs;
1179 alias linestart = line2pos; /// ditto
1181 /// get ending position for the given line (position of '\n')
1182 /// it may be `textsize`, though, if this is the last line, and it doesn't end with '\n'
1183 int lineend (int lidx) nothrow {
1184 if (lidx < 0 || gb.textsize == 0) return 0;
1185 if (lidx > mLineCount-1) return gb.textsize;
1186 if (mLineCount == 1) {
1187 assert(lidx == 0);
1188 return gb.textsize;
1190 if (lidx == mLineCount-1) return gb.textsize;
1191 auto res = locache[lidx+1].ofs;
1192 assert(res > 0);
1193 return res-1;
1196 // move by `x` utfucked chars
1197 // `pos` should point to line start
1198 // will never go beyond EOL
1199 private int utfuck_x2pos (int x, int pos) nothrow {
1200 immutable ts = gb.textsize;
1201 const(char)* tbuf = gb.tbuf;
1202 if (pos < 0) pos = 0;
1203 if (mWordWrapWidth <= 0 || gb.mSingleLine) {
1204 if (gb.mSingleLine) {
1205 // single line
1206 while (pos < ts && x > 0) {
1207 pos += gb.utfuckLenAt!true(pos); // "always positive"
1208 --x;
1210 } else {
1211 // multiline
1212 while (pos < ts && x > 0) {
1213 if (tbuf[gb.pos2real(pos)] == '\n') break;
1214 pos += gb.utfuckLenAt!true(pos); // "always positive"
1215 --x;
1218 } else {
1219 if (pos >= ts) return ts;
1220 int lidx = findLineCacheIndex(pos);
1221 assert(lidx >= 0);
1222 int epos = locache[lidx+1].ofs;
1223 while (pos < epos && x > 0) {
1224 if (tbuf[gb.pos2real(pos)] == '\n') break;
1225 pos += gb.utfuckLenAt!true(pos); // "always positive"
1226 --x;
1229 if (pos > ts) pos = ts;
1230 return pos;
1233 // convert line offset to screen x coordinate
1234 // `pos` should point into line (somewhere)
1235 private int utfuck_pos2x(bool dotabs=false) (int pos) nothrow {
1236 immutable ts = gb.textsize;
1237 if (pos < 0) pos = 0;
1238 if (pos > ts) pos = ts;
1239 immutable bool sl = gb.mSingleLine;
1240 const(char)* tbuf = gb.tbuf;
1241 int x = 0;
1242 if (mWordWrapWidth <= 0) {
1243 // find line start
1244 int spos = pos;
1245 if (!sl) {
1246 while (spos > 0 && tbuf[gb.pos2real(spos-1)] != '\n') --spos;
1247 } else {
1248 spos = 0;
1250 // now `spos` points to line start; walk over utfucked chars
1251 while (spos < pos) {
1252 char ch = tbuf[gb.pos2real(spos)];
1253 if (!sl && ch == '\n') break;
1254 static if (dotabs) {
1255 if (ch == '\t' && visualtabs && tabsize > 0) {
1256 x = ((x+tabsize)/tabsize)*tabsize;
1257 } else {
1258 ++x;
1260 } else {
1261 ++x;
1263 spos += (ch < 128 ? 1 : gb.utfuckLenAt!true(spos));
1265 } else {
1266 // word-wrapped, eh...
1267 int lidx = findLineCacheIndex(pos);
1268 assert(lidx >= 0);
1269 int spos = locache[lidx].ofs;
1270 int epos = locache[lidx+1].ofs;
1271 assert(pos < epos);
1272 // now `spos` points to line start; walk over utfucked chars
1273 while (spos < pos) {
1274 char ch = tbuf[gb.pos2real(spos)];
1275 if (!sl && ch == '\n') break;
1276 static if (dotabs) {
1277 if (ch == '\t' && visualtabs && tabsize > 0) {
1278 x = ((x+tabsize)/tabsize)*tabsize;
1279 } else {
1280 ++x;
1282 } else {
1283 ++x;
1285 spos += (ch < 128 ? 1 : gb.utfuckLenAt!true(spos));
1288 return x;
1291 /// get position for the given text coordinates
1292 int xy2pos (int x, int y) nothrow {
1293 auto ts = gb.textsize;
1294 if (ts == 0 || y < 0) return 0;
1295 if (y > mLineCount-1) return ts;
1296 if (x < 0) x = 0;
1297 if (mLineCount == 1) {
1298 assert(y == 0);
1299 return (!utfuck ? (x < ts ? x : ts) : utfuck_x2pos(x, 0));
1301 uint ls = locache[y].ofs;
1302 uint le = locache[y+1].ofs;
1303 if (ls == le) {
1304 // this should be last empty line
1305 //if (y != mLineCount-1) { import std.format; assert(0, "fuuuuu; y=%u; lc=%u; locused=%u".format(y, mLineCount, locused)); }
1306 assert(y == mLineCount-1);
1307 return ls;
1309 if (!utfuck) {
1310 // we want line end (except for last empty line, where we want end-of-text)
1311 if (x >= le-ls) return (y != mLineCount-1 ? le-1 : le);
1312 return ls+x; // somewhere in line
1313 } else {
1314 // fuck
1315 return utfuck_x2pos(x, ls);
1319 /// get text coordinates for the given position
1320 void pos2xy (int pos, out int x, out int y) nothrow {
1321 immutable ts = gb.textsize;
1322 if (pos <= 0 || ts == 0) return; // x and y autoinited
1323 if (pos > ts) pos = ts;
1324 if (mLineCount == 1) {
1325 // y is autoinited
1326 x = (!utfuck ? pos : utfuck_pos2x(pos));
1327 return;
1329 const(char)* tbuf = gb.tbuf;
1330 if (pos == ts) {
1331 // end of text: no need to update line offset cache
1332 y = mLineCount-1;
1333 if (!gb.mSingleLine) {
1334 while (pos > 0 && tbuf[gb.pos2real(--pos)] != '\n') ++x;
1335 } else {
1336 x = pos;
1338 return;
1340 int lcidx = findLineCacheIndex(pos);
1341 assert(lcidx >= 0 && lcidx < mLineCount);
1342 immutable ls = locache[lcidx].ofs;
1343 //auto le = lineofsc[lcidx+1];
1344 //!assert(pos >= ls && pos < le);
1345 y = cast(uint)lcidx;
1346 x = (!utfuck ? pos-ls : utfuck_pos2x(pos));
1349 /// get text coordinates (adjusted for tabs) for the given position
1350 void pos2xyVT (int pos, out int x, out int y) nothrow {
1351 if (!utfuck && (!visualtabs || tabsize == 0)) { pos2xy(pos, x, y); return; }
1353 void tabbedX() (int ls) {
1354 x = 0;
1355 version(none) {
1356 //TODO:FIXME: fix this!
1357 while (ls < pos) {
1358 int tp = fastFindCharIn(ls, pos-ls, '\t');
1359 if (tp < 0) { x += pos-ls; return; }
1360 x += tp-ls;
1361 ls = tp;
1362 while (ls < pos && tbuf[pos2real(ls++)] == '\t') {
1363 x = ((x+tabsize)/tabsize)*tabsize;
1366 } else {
1367 const(char)* tbuf = gb.tbuf;
1368 while (ls < pos) {
1369 if (tbuf[gb.pos2real(ls++)] == '\t') x = ((x+tabsize)/tabsize)*tabsize; else ++x;
1374 auto ts = gb.textsize;
1375 if (pos <= 0 || ts == 0) return; // x and y autoinited
1376 if (pos > ts) pos = ts;
1377 if (mLineCount == 1) {
1378 // y is autoinited
1379 if (utfuck) { x = utfuck_pos2x!true(pos); return; }
1380 if (!visualtabs || tabsize == 0) { x = pos; return; }
1381 tabbedX(0);
1382 return;
1384 if (pos == ts) {
1385 // end of text: no need to update line offset cache
1386 const(char)* tbuf = gb.tbuf;
1387 y = mLineCount-1;
1388 while (pos > 0 && (gb.mSingleLine || tbuf[gb.pos2real(--pos)] != '\n')) ++x;
1389 if (utfuck) { x = utfuck_pos2x!true(ts); return; }
1390 if (visualtabs && tabsize != 0) { int ls = pos+1; pos = ts; tabbedX(ls); return; }
1391 return;
1393 int lcidx = findLineCacheIndex(pos);
1394 assert(lcidx >= 0 && lcidx < mLineCount);
1395 auto ls = locache[lcidx].ofs;
1396 //auto le = lineofsc[lcidx+1];
1397 //!assert(pos >= ls && pos < le);
1398 y = cast(uint)lcidx;
1399 if (utfuck) { x = utfuck_pos2x!true(pos); return; }
1400 if (visualtabs && tabsize > 0) { tabbedX(ls); return; }
1401 x = pos-ls;
1406 // ////////////////////////////////////////////////////////////////////////// //
1407 private final class UndoStack {
1408 public:
1409 enum Type : ubyte {
1410 None,
1412 CurMove, // pos: old position; len: old topline (sorry)
1413 TextRemove, // pos: position; len: length; deleted chars follows
1414 TextInsert, // pos: position; len: length
1415 // grouping
1416 GroupStart,
1417 GroupEnd,
1420 private:
1421 static align(1) struct Action {
1422 align(1):
1423 enum Flag : ubyte {
1424 BlockMarking = 1<<0, // block marking state
1425 LastBE = 1<<1, // last block move was at end?
1426 Changed = 1<<2, // "changed" flag
1427 //VisTabs = 1<<3, // editor was in "visual tabs" mode
1428 CurAtEdge = 1<<4, // cursor was at the block edge, use `edgex, edgey` to position it
1431 @property nothrow pure @safe @nogc {
1432 bool bmarking () const { pragma(inline, true); return (flags&Flag.BlockMarking) != 0; }
1433 bool lastbe () const { pragma(inline, true); return (flags&Flag.LastBE) != 0; }
1434 bool txchanged () const { pragma(inline, true); return (flags&Flag.Changed) != 0; }
1435 //bool vistabs () const { pragma(inline, true); return (flags&Flag.VisTabs) != 0; }
1436 bool curatedge () const { pragma(inline, true); return (flags&Flag.CurAtEdge) != 0; }
1438 void bmarking (bool v) { pragma(inline, true); if (v) flags |= Flag.BlockMarking; else flags &= ~(Flag.BlockMarking); }
1439 void lastbe (bool v) { pragma(inline, true); if (v) flags |= Flag.LastBE; else flags &= ~(Flag.LastBE); }
1440 void txchanged (bool v) { pragma(inline, true); if (v) flags |= Flag.Changed; else flags &= ~(Flag.Changed); }
1441 //void vistabs (bool v) { pragma(inline, true); if (v) flags |= Flag.VisTabs; else flags &= ~Flag.VisTabs; }
1442 void curatedge (bool v) { pragma(inline, true); if (v) flags |= Flag.CurAtEdge; else flags &= ~(Flag.CurAtEdge); }
1445 Type type;
1446 int pos;
1447 int len;
1448 // after undoing action
1449 int cx, cy, topline, xofs;
1450 int bs, be; // block position
1451 int edgecurpos, edgetopline/*, edgexofs*/; // "edge" cursor postion
1452 ubyte flags;
1453 // data follows
1454 char[0] data;
1457 version(Posix) private import core.sys.posix.unistd : off_t;
1459 private:
1460 version(Posix) int tmpfd = -1; else enum tmpfd = -1;
1461 version(Posix) off_t tmpsize = 0;
1462 bool asRedo;
1463 // undo buffer format:
1464 // last uint is always record size (not including size uints); record follows (up), then size again
1465 uint maxBufSize = 32*1024*1024;
1466 ubyte* undoBuffer;
1467 uint ubUsed, ubSize;
1468 bool asRich;
1470 final:
1471 version(Posix) void initTempFD () nothrow {
1472 import core.sys.posix.fcntl /*: open*/;
1473 static if (is(typeof(O_CLOEXEC)) && is(typeof(O_TMPFILE))) {
1474 auto xfd = open("/tmp/_egundoz", O_RDWR|O_CLOEXEC|O_TMPFILE, 0x1b6/*0o600*/);
1475 if (xfd < 0) return;
1476 tmpfd = xfd;
1477 tmpsize = 0;
1481 // returns record size
1482 version(Posix) uint loadLastRecord(bool fullrecord=true) (bool dropit=false) nothrow {
1483 import core.stdc.stdio : SEEK_SET, SEEK_END;
1484 import core.sys.posix.unistd : lseek, read;
1485 assert(tmpfd >= 0);
1486 uint sz;
1487 if (tmpsize < sz.sizeof) return 0;
1488 lseek(tmpfd, tmpsize-sz.sizeof, SEEK_SET);
1489 if (read(tmpfd, &sz, sz.sizeof) != sz.sizeof) return 0;
1490 if (tmpsize < sz+sz.sizeof*2) return 0;
1491 if (sz < Action.sizeof) return 0;
1492 lseek(tmpfd, tmpsize-sz-sz.sizeof, SEEK_SET);
1493 static if (fullrecord) {
1494 alias rsz = sz;
1495 } else {
1496 auto rsz = cast(uint)Action.sizeof;
1498 if (ubSize < rsz) {
1499 import core.stdc.stdlib : realloc;
1500 auto nb = cast(ubyte*)realloc(undoBuffer, rsz);
1501 if (nb is null) return 0;
1502 undoBuffer = nb;
1503 ubSize = rsz;
1505 ubUsed = rsz;
1506 if (read(tmpfd, undoBuffer, rsz) != rsz) return 0;
1507 if (dropit) tmpsize -= sz+sz.sizeof*2;
1508 return rsz;
1511 bool saveLastRecord () nothrow {
1512 version(Posix) {
1513 import core.stdc.stdio : SEEK_SET;
1514 import core.sys.posix.unistd : lseek, write;
1515 if (tmpfd >= 0) {
1516 assert(ubUsed >= Action.sizeof);
1517 scope(exit) {
1518 import core.stdc.stdlib : free;
1519 if (ubUsed > 65536) {
1520 free(undoBuffer);
1521 undoBuffer = null;
1522 ubUsed = ubSize = 0;
1525 auto ofs = lseek(tmpfd, tmpsize, SEEK_SET);
1526 if (write(tmpfd, &ubUsed, ubUsed.sizeof) != ubUsed.sizeof) return false;
1527 if (write(tmpfd, undoBuffer, ubUsed) != ubUsed) return false;
1528 if (write(tmpfd, &ubUsed, ubUsed.sizeof) != ubUsed.sizeof) return false;
1529 write(tmpfd, &tmpsize, tmpsize.sizeof);
1530 tmpsize += ubUsed+uint.sizeof*2;
1533 return true;
1536 // return `true` if something was removed
1537 bool removeFirstUndo () nothrow {
1538 import core.stdc.string : memmove;
1539 version(Posix) assert(tmpfd < 0);
1540 if (ubUsed == 0) return false;
1541 uint np = (*cast(uint*)undoBuffer)+4*2;
1542 assert(np <= ubUsed);
1543 if (np == ubUsed) { ubUsed = 0; return true; }
1544 memmove(undoBuffer, undoBuffer+np, ubUsed-np);
1545 ubUsed -= np;
1546 return true;
1549 // return `null` if it can't; undo buffer is in invalid state then
1550 Action* addUndo (int dataSize) nothrow {
1551 import core.stdc.stdlib : realloc;
1552 import core.stdc.string : memset;
1553 version(Posix) if (tmpfd < 0) {
1554 if (dataSize < 0 || dataSize >= maxBufSize) return null; // no room
1555 uint asz = cast(uint)Action.sizeof+dataSize+4*2;
1556 if (asz > maxBufSize) return null;
1557 if (ubSize-ubUsed < asz) {
1558 uint nasz = ubUsed+asz;
1559 if (nasz&0xffff) nasz = (nasz|0xffff)+1;
1560 if (nasz > maxBufSize) {
1561 while (ubSize-ubUsed < asz) { if (!removeFirstUndo()) return null; }
1562 } else {
1563 auto nb = cast(ubyte*)realloc(undoBuffer, nasz);
1564 if (nb is null) {
1565 while (ubSize-ubUsed < asz) { if (!removeFirstUndo()) return null; }
1566 } else {
1567 undoBuffer = nb;
1568 ubSize = nasz;
1572 assert(ubSize-ubUsed >= asz);
1573 *cast(uint*)(undoBuffer+ubUsed) = asz-4*2;
1574 auto res = cast(Action*)(undoBuffer+ubUsed+4);
1575 *cast(uint*)(undoBuffer+ubUsed+asz-4) = asz-4*2;
1576 ubUsed += asz;
1577 memset(res, 0, asz-4*2);
1578 return res;
1581 // has temp file
1582 if (dataSize < 0 || dataSize >= int.max/4) return null; // wtf?!
1583 uint asz = cast(uint)Action.sizeof+dataSize;
1584 if (ubSize < asz) {
1585 auto nb = cast(ubyte*)realloc(undoBuffer, asz);
1586 if (nb is null) return null;
1587 undoBuffer = nb;
1588 ubSize = asz;
1590 ubUsed = asz;
1591 auto res = cast(Action*)undoBuffer;
1592 memset(res, 0, asz);
1593 return res;
1597 // can return null
1598 Action* lastUndoHead () nothrow {
1599 version(Posix) if (tmpfd >= 0) {
1600 if (loadLastRecord!false()) return null;
1601 return cast(Action*)undoBuffer;
1604 if (ubUsed == 0) return null;
1605 auto sz = *cast(uint*)(undoBuffer+ubUsed-4);
1606 return cast(Action*)(undoBuffer+ubUsed-4-sz);
1610 Action* popUndo () nothrow {
1611 version(Posix) if (tmpfd >= 0) {
1612 auto len = loadLastRecord!true(true); // pop it
1613 return (len ? cast(Action*)undoBuffer : null);
1616 if (ubUsed == 0) return null;
1617 auto sz = *cast(uint*)(undoBuffer+ubUsed-4);
1618 auto res = cast(Action*)(undoBuffer+ubUsed-4-sz);
1619 ubUsed -= sz+4*2;
1620 return res;
1624 public:
1625 this (bool aAsRich, bool aAsRedo, bool aIntoFile) nothrow {
1626 asRedo = aAsRedo;
1627 asRich = aAsRich;
1628 if (aIntoFile) {
1629 initTempFD();
1630 if (tmpfd < 0) {
1631 //version(aliced) { import iv.rawtty; ttyBeep(); }
1636 ~this () nothrow {
1637 import core.stdc.stdlib : free;
1638 import core.sys.posix.unistd : close;
1639 if (tmpfd >= 0) { close(tmpfd); tmpfd = -1; }
1640 if (undoBuffer !is null) free(undoBuffer);
1643 void clear (bool doclose=false) nothrow {
1644 ubUsed = 0;
1645 if (doclose) {
1646 version(Posix) {
1647 import core.stdc.stdlib : free;
1648 if (tmpfd >= 0) {
1649 import core.sys.posix.unistd : close;
1650 close(tmpfd);
1651 tmpfd = -1;
1654 if (undoBuffer !is null) free(undoBuffer);
1655 undoBuffer = null;
1656 ubSize = 0;
1657 } else {
1658 if (ubSize > 65536) {
1659 import core.stdc.stdlib : realloc;
1660 auto nb = cast(ubyte*)realloc(undoBuffer, 65536);
1661 if (nb !is null) {
1662 undoBuffer = nb;
1663 ubSize = 65536;
1666 version(Posix) if (tmpfd >= 0) tmpsize = 0;
1670 void alwaysChanged () nothrow {
1671 if (tmpfd < 0) {
1672 auto pos = 0;
1673 while (pos < ubUsed) {
1674 auto sz = *cast(uint*)(undoBuffer+pos);
1675 auto res = cast(Action*)(undoBuffer+pos+4);
1676 pos += sz+4*2;
1677 switch (res.type) {
1678 case Type.TextRemove:
1679 case Type.TextInsert:
1680 res.txchanged = true;
1681 break;
1682 default:
1685 } else {
1686 version(Posix) {
1687 import core.stdc.stdio : SEEK_SET;
1688 import core.sys.posix.unistd : lseek, read, write;
1689 off_t cpos = 0;
1690 Action act;
1691 while (cpos < tmpsize) {
1692 uint sz;
1693 lseek(tmpfd, cpos, SEEK_SET);
1694 if (read(tmpfd, &sz, sz.sizeof) != sz.sizeof) break;
1695 if (sz < Action.sizeof) assert(0, "wtf?!");
1696 if (read(tmpfd, &act, Action.sizeof) != Action.sizeof) break;
1697 switch (act.type) {
1698 case Type.TextRemove:
1699 case Type.TextInsert:
1700 if (act.txchanged != true) {
1701 act.txchanged = true;
1702 lseek(tmpfd, cpos+sz.sizeof, SEEK_SET);
1703 write(tmpfd, &act, Action.sizeof);
1705 break;
1706 default:
1708 cpos += sz+sz.sizeof*2;
1714 private void fillCurPos (Action* ua, EditorEngine ed) nothrow {
1715 if (ua !is null && ed !is null) {
1716 //TODO: correct x according to "visual tabs" mode (i.e. make it "normal x")
1717 ua.cx = ed.cx;
1718 ua.cy = ed.cy;
1719 ua.topline = ed.mTopLine;
1720 ua.xofs = ed.mXOfs;
1721 ua.bs = ed.bstart;
1722 ua.be = ed.bend;
1723 ua.bmarking = ed.markingBlock;
1724 ua.lastbe = ed.lastBGEnd;
1725 ua.txchanged = ed.txchanged;
1726 // just in case
1727 ua.curatedge = false;
1728 //ua.edgexofs = ed.mXOfs;
1729 //ua.vistabs = ed.visualtabs;
1730 // check if the cursor is at the edge of the block
1731 if (!asRedo && (ua.type == Type.TextInsert || ua.type == Type.TextRemove)) {
1732 auto cpos = ed.curpos;
1733 ua.edgecurpos = cpos;
1734 ua.edgetopline = ua.topline;
1735 if (ua.type == Type.TextInsert) {
1736 // inserting text, is cursor at the insert position?
1737 if (cpos == ua.pos) {
1738 // position it at the end, because undo will remove this text
1739 ua.curatedge = true;
1740 ua.edgecurpos = ua.pos+ua.len;
1742 } else if (ua.type == Type.TextRemove) {
1743 // deleting text, at the beginning, inside, or immediately after?
1744 if (cpos >= ua.pos && cpos <= ua.pos+ua.len) {
1745 // position it at the start, because undo will insert this text
1746 ua.curatedge = true;
1747 ua.edgecurpos = ua.pos;
1754 version(egeditor_record_movement_undo)
1755 bool addCurMove (EditorEngine ed, bool fromRedo=false) nothrow {
1756 if (auto lu = lastUndoHead()) {
1757 if (lu.type == Type.CurMove) {
1758 if (lu.cx == ed.cx && lu.cy == ed.cy && lu.topline == ed.mTopLine && lu.xofs == ed.mXOfs &&
1759 lu.bs == ed.bstart && lu.be == ed.bend && lu.bmarking == ed.markingBlock &&
1760 lu.lastbe == ed.lastBGEnd /*&& lu.vistabs == ed.visualtabs*/) return true;
1763 if (!asRedo && !fromRedo && ed.redo !is null) ed.redo.clear();
1764 auto act = addUndo(0);
1765 if (act is null) { clear(); return false; }
1766 act.type = Type.CurMove;
1767 fillCurPos(act, ed);
1768 return saveLastRecord();
1771 bool addTextRemove (EditorEngine ed, int pos, int count, bool fromRedo=false) nothrow {
1772 if (!asRedo && !fromRedo && ed.redo !is null) ed.redo.clear();
1773 GapBuffer gb = ed.gb;
1774 assert(gb !is null);
1775 if (pos < 0 || pos >= gb.textsize) return true;
1776 if (count < 1) return true;
1777 if (count >= maxBufSize) { clear(); return false; }
1778 if (count > gb.textsize-pos) { clear(); return false; }
1779 int realcount = count;
1780 if (asRich && realcount > 0) {
1781 if (realcount >= int.max/gb.hbuf[0].sizeof/2) return false;
1782 realcount += realcount*cast(int)gb.hbuf[0].sizeof;
1784 auto act = addUndo(realcount);
1785 if (act is null) { clear(); return false; }
1786 act.type = Type.TextRemove;
1787 act.pos = pos;
1788 act.len = count;
1789 fillCurPos(act, ed);
1790 auto dp = act.data.ptr;
1791 while (count--) *dp++ = gb[pos++];
1792 // save attrs for rich editor
1793 if (asRich && realcount > 0) {
1794 pos = act.pos;
1795 count = act.len;
1796 auto dph = cast(GapBuffer.HighState*)dp;
1797 while (count--) *dph++ = gb.hi(pos++);
1799 return saveLastRecord();
1802 bool addTextInsert (EditorEngine ed, int pos, int count, bool fromRedo=false) nothrow {
1803 if (!asRedo && !fromRedo && ed.redo !is null) ed.redo.clear();
1804 auto act = addUndo(0);
1805 if (act is null) { clear(); return false; }
1806 act.type = Type.TextInsert;
1807 act.pos = pos;
1808 act.len = count;
1809 fillCurPos(act, ed);
1810 return saveLastRecord();
1813 bool addGroupStart (EditorEngine ed, bool fromRedo=false) nothrow {
1814 if (!asRedo && !fromRedo && ed.redo !is null) ed.redo.clear();
1815 auto act = addUndo(0);
1816 if (act is null) { clear(); return false; }
1817 act.type = Type.GroupStart;
1818 fillCurPos(act, ed);
1819 return saveLastRecord();
1822 bool addGroupEnd (EditorEngine ed, bool fromRedo=false) nothrow {
1823 if (!asRedo && !fromRedo && ed.redo !is null) ed.redo.clear();
1824 auto act = addUndo(0);
1825 if (act is null) { clear(); return false; }
1826 act.type = Type.GroupEnd;
1827 fillCurPos(act, ed);
1828 return saveLastRecord();
1831 @property bool hasUndo () const pure nothrow @safe @nogc { pragma(inline, true); return (tmpfd < 0 ? (ubUsed > 0) : (tmpsize > 0)); }
1833 private bool copyAction (Action* ua) nothrow {
1834 import core.stdc.string : memcpy;
1835 if (ua is null) return true;
1836 auto na = addUndo(ua.type == Type.TextRemove ? ua.len : 0);
1837 if (na is null) return false;
1838 memcpy(na, ua, Action.sizeof+(ua.type == Type.TextRemove ? ua.len : 0));
1839 return saveLastRecord();
1842 // return "None" in case of error
1843 Type undoAction (EditorEngine ed) {
1844 UndoStack oppos = (asRedo ? ed.undo : ed.redo);
1845 assert(ed !is null);
1846 version(egeditor_record_movement_undo) {
1847 auto ua = popUndo();
1848 } else {
1849 // actions are done in two way: first, move cursor to where it should be, then perform the action
1850 auto ua = lastUndoHead();
1852 if (ua is null) return Type.None;
1853 //debug(egauto) if (!asRedo) { { import iv.vfs; auto fo = VFile("z00_undo.bin", "a"); fo.writeln(*ua); } }
1854 Type res = ua.type;
1855 version(egeditor_record_movement_undo) {
1856 // nothing interesting here
1857 } else {
1858 // move cursor to the correct position first
1859 if (!asRedo && ua.curatedge && (res == Type.TextInsert || res == Type.TextRemove)) {
1860 // just in case, reset "curatedge"
1861 ua.curatedge = false;
1862 auto cpos = ed.curpos;
1863 if (cpos != ua.edgecurpos) {
1864 // reposition the cursor
1865 ed.markingBlock = false;
1866 int cpx, cpy;
1867 ed.pos2xy(ua.edgecurpos, out cpx, out cpy);
1868 ed.cx = cpx;
1869 ed.cy = cpy;
1870 ed.mTopLine = ua.edgetopline;
1871 //ed.mXOfs = ua.edgexofs; //???
1872 ed.makeCurLineVisible();
1873 return Type.CurMove; // yeah
1876 // otherwise, pop and perform the action
1877 ua = popUndo();
1878 assert(ua !is null);
1880 final switch (ua.type) {
1881 case Type.None: assert(0, "wtf?!");
1882 case Type.GroupStart:
1883 case Type.GroupEnd:
1884 if (oppos !is null) { if (!oppos.copyAction(ua)) oppos.clear(); }
1885 break;
1886 case Type.CurMove:
1887 version(egeditor_record_movement_undo) {
1888 if (oppos !is null) { if (oppos.addCurMove(ed, asRedo) == Type.None) oppos.clear(); }
1889 break;
1890 } else {
1891 assert(0, "egeditor undo: this should never happen");
1893 case Type.TextInsert: // remove inserted text
1894 if (oppos !is null) { if (oppos.addTextRemove(ed, ua.pos, ua.len, asRedo) == Type.None) oppos.clear(); }
1895 //ed.writeLogAction(ua.pos, -ua.len);
1896 ed.ubTextRemove(ua.pos, ua.len);
1897 break;
1898 case Type.TextRemove: // insert removed text
1899 if (oppos !is null) { if (oppos.addTextInsert(ed, ua.pos, ua.len, asRedo) == Type.None) oppos.clear(); }
1900 if (ed.ubTextInsert(ua.pos, ua.data.ptr[0..ua.len])) {
1901 if (asRich) ed.ubTextSetAttrs(ua.pos, (cast(GapBuffer.HighState*)(ua.data.ptr+ua.len))[0..ua.len]);
1903 //ed.writeLogAction(ua.pos, ua.len);
1904 break;
1906 //FIXME: optimize redraw
1907 if (ua.bs != ed.bstart || ua.be != ed.bend) {
1908 if (ua.bs < ua.be) {
1909 // undo has block
1910 if (ed.bstart < ed.bend) ed.markLinesDirtySE(ed.lc.pos2line(ed.bstart), ed.lc.pos2line(ed.bend)); // old block is dirty
1911 ed.markLinesDirtySE(ed.lc.pos2line(ua.bs), ed.lc.pos2line(ua.be)); // new block is dirty
1912 } else {
1913 // undo has no block
1914 if (ed.bstart < ed.bend) ed.markLinesDirtySE(ed.lc.pos2line(ed.bstart), ed.lc.pos2line(ed.bend)); // old block is dirty
1917 ed.bstart = ua.bs;
1918 ed.bend = ua.be;
1919 ed.markingBlock = ua.bmarking;
1920 ed.lastBGEnd = ua.lastbe;
1921 // don't restore "visual tabs" mode
1922 //TODO: correct x according to "visual tabs" mode (i.e. make it "visual x")
1923 ed.cx = ua.cx;
1924 ed.cy = ua.cy;
1925 ed.mTopLine = ua.topline;
1926 ed.mXOfs = ua.xofs;
1927 ed.txchanged = ua.txchanged;
1928 return res;
1933 // ////////////////////////////////////////////////////////////////////////// //
1934 /// main editor engine: does all the undo/redo magic, block management, etc.
1935 class EditorEngine {
1936 public:
1938 enum CodePage : ubyte {
1939 koi8u, ///
1940 cp1251, ///
1941 cp866, ///
1943 CodePage codepage = CodePage.koi8u; ///
1945 /// from koi to codepage
1946 final char recodeCharTo (char ch) pure const nothrow {
1947 pragma(inline, true);
1948 return
1949 codepage == CodePage.cp1251 ? uni2cp1251(koi2uni(ch)) :
1950 codepage == CodePage.cp866 ? uni2cp866(koi2uni(ch)) :
1954 /// from codepage to koi
1955 final char recodeCharFrom (char ch) pure const nothrow {
1956 pragma(inline, true);
1957 return
1958 codepage == CodePage.cp1251 ? uni2koi(cp12512uni(ch)) :
1959 codepage == CodePage.cp866 ? uni2koi(cp8662uni(ch)) :
1963 /// recode to codepage
1964 final char recodeU2B (dchar dch) pure const nothrow {
1965 final switch (codepage) {
1966 case CodePage.koi8u: return uni2koi(dch);
1967 case CodePage.cp1251: return uni2cp1251(dch);
1968 case CodePage.cp866: return uni2cp866(dch);
1972 /// should not be called for utfuck mode
1973 final dchar recode1b (char ch) pure const nothrow {
1974 final switch (codepage) {
1975 case CodePage.koi8u: return koi2uni(ch);
1976 case CodePage.cp1251: return cp12512uni(ch);
1977 case CodePage.cp866: return cp8662uni(ch);
1981 protected:
1982 int lineHeightPixels = 0; /// <0: use line height API, proportional fonts; 0: everything is cell-based (tty); >0: constant line height in pixels; proprotional fonts
1983 int prevTopLine = -1;
1984 int mTopLine = 0;
1985 int prevXOfs = -1;
1986 int mXOfs = 0;
1987 int cx, cy;
1988 int[] dirtyLines; // line heights or 0 if not dirty; hack!
1989 int winx, winy, winw, winh;
1990 GapBuffer gb;
1991 LineCache lc;
1992 EditorHL hl;
1993 UndoStack undo, redo;
1994 int bstart = -1, bend = -1; // marked block position
1995 bool markingBlock;
1996 bool lastBGEnd; // last block grow was at end?
1997 bool txchanged;
1998 bool mReadOnly; // has any effect only if you are using `insertText()` and `deleteText()` API!
1999 bool mSingleLine; // has any effect only if you are using `insertText()` and `deleteText()` API!
2000 bool mKillTextOnChar; // mostly for single-line: remove all text on new char; will autoreset on move
2002 char[] indentText; // this buffer is actively reused, do not expose!
2003 int inPasteMode;
2005 bool mAsRich; /// this is "rich editor", so engine should save/restore highlighting info in undo
2007 protected:
2008 bool[int] linebookmarked; /// is this line bookmarked?
2010 public:
2011 //EgTextMeter textMeter; /// *MUST* be set when `inPixels` is true
2012 final @property EgTextMeter textMeter () nothrow @nogc { return lc.textMeter; }
2013 final @property void textMeter (EgTextMeter tm) nothrow @nogc { lc.textMeter = tm; }
2015 final @property int wordWrapPos () const nothrow @nogc { pragma(inline, true); return lc.mWordWrapWidth; }
2016 final @property void wordWrapPos (int v) nothrow {
2017 if (v < 0) v = 0;
2018 if (lc.mWordWrapWidth != v) {
2019 auto pos = curpos;
2020 lc.mWordWrapWidth = v;
2021 lc.rebuild();
2022 gotoPos!true(pos);
2026 public:
2027 /// is editor in "paste mode" (i.e. we are pasting chars from clipboard, and should skip autoindenting)?
2028 final @property bool pasteMode () const pure nothrow @safe @nogc { return (inPasteMode > 0); }
2029 final resetPasteMode () pure nothrow @safe @nogc { inPasteMode = 0; } /// reset "paste mode"
2032 void clearBookmarks () nothrow { linebookmarked.clear(); }
2034 enum BookmarkChangeMode { Toggle, Set, Reset } ///
2037 void bookmarkChange (int cy, BookmarkChangeMode mode) nothrow {
2038 if (cy < 0 || cy >= lc.linecount) return;
2039 if (mSingleLine) return; // ignore for single-line mode
2040 final switch (mode) {
2041 case BookmarkChangeMode.Toggle:
2042 if (cy in linebookmarked) linebookmarked.remove(cy); else linebookmarked[cy] = true;
2043 markLinesDirty(cy, 1);
2044 break;
2045 case BookmarkChangeMode.Set:
2046 if (cy !in linebookmarked) {
2047 linebookmarked[cy] = true;
2048 markLinesDirty(cy, 1);
2050 break;
2051 case BookmarkChangeMode.Reset:
2052 if (cy in linebookmarked) {
2053 linebookmarked.remove(cy);
2054 markLinesDirty(cy, 1);
2056 break;
2061 final void doBookmarkToggle () nothrow { pragma(inline, true); bookmarkChange(cy, BookmarkChangeMode.Toggle); }
2064 final @property bool isLineBookmarked (int lidx) nothrow {
2065 pragma(inline, true);
2066 return ((lidx in linebookmarked) !is null);
2070 final void doBookmarkJumpUp () nothrow {
2071 int bestBM = -1;
2072 foreach (int lidx; linebookmarked.byKey) {
2073 if (lidx < cy && lidx > bestBM) bestBM = lidx;
2075 if (bestBM >= 0) {
2076 version(egeditor_record_movement_undo) pushUndoCurPos();
2077 cy = bestBM;
2078 normXY;
2079 growBlockMark();
2080 makeCurLineVisibleCentered();
2085 final void doBookmarkJumpDown () nothrow {
2086 int bestBM = int.max;
2087 foreach (int lidx; linebookmarked.byKey) {
2088 if (lidx > cy && lidx < bestBM) bestBM = lidx;
2090 if (bestBM < lc.linecount) {
2091 version(egeditor_record_movement_undo) pushUndoCurPos();
2092 cy = bestBM;
2093 normXY;
2094 growBlockMark();
2095 makeCurLineVisibleCentered();
2099 ///WARNING! don't mutate bookmarks here!
2100 final void forEachBookmark (scope void delegate (int lidx) dg) {
2101 if (dg is null) return;
2102 foreach (int lidx; linebookmarked.byKey) dg(lidx);
2105 /// call this from `willBeDeleted()` (only!) to fix bookmarks
2106 final void bookmarkDeletionFix (int pos, int len, int eolcount) nothrow {
2107 if (eolcount && linebookmarked.length > 0) {
2108 import core.stdc.stdlib : malloc, free;
2109 // remove bookmarks whose lines are removed, move other bookmarks
2110 auto py = lc.pos2line(pos);
2111 auto ey = lc.pos2line(pos+len);
2112 bool wholeFirstLineDeleted = (pos == lc.line2pos(py)); // do we want to remove the whole first line?
2113 bool wholeLastLineDeleted = (pos+len == lc.line2pos(ey)); // do we want to remove the whole last line?
2114 if (wholeLastLineDeleted) --ey; // yes, `ey` is one line down the last, fix it
2115 // build new bookmark array
2116 int* newbm = cast(int*)malloc(int.sizeof*linebookmarked.length);
2117 if (newbm !is null) {
2118 scope(exit) free(newbm);
2119 int newbmpos = 0;
2120 bool smthWasChanged = false;
2121 foreach (int lidx; linebookmarked.byKey) {
2122 // remove "first line" bookmark if "first line" is deleted
2123 if (wholeFirstLineDeleted && lidx == py) { smthWasChanged = true; continue; }
2124 // remove "last line" bookmark if "last line" is deleted
2125 if (wholeLastLineDeleted && lidx == ey) { smthWasChanged = true; continue; }
2126 // remove bookmarks that are in range
2127 if (lidx > py && lidx < ey) continue;
2128 // fix bookmark line if necessary
2129 if (lidx >= ey) { smthWasChanged = true; lidx -= eolcount; }
2130 if (lidx >= 0 && lidx < lc.linecount) {
2131 //assert(lidx >= 0 && lidx < lc.linecount);
2132 // add this bookmark to new list
2133 newbm[newbmpos++] = lidx;
2136 // rebuild list if something was changed
2137 if (smthWasChanged) {
2138 fullDirty(); //TODO: optimize this
2139 linebookmarked.clear;
2140 foreach (int lidx; newbm[0..newbmpos]) linebookmarked[lidx] = true;
2142 } else {
2143 // out of memory, what to do? just clear bookmarks for now
2144 linebookmarked.clear;
2145 fullDirty(); // just in case
2150 /// call this from `willBeInserted()` or `wasInserted()` to fix bookmarks
2151 final void bookmarkInsertionFix (int pos, int len, int eolcount) nothrow {
2152 if (eolcount && linebookmarked.length > 0) {
2153 import core.stdc.stdlib : malloc, free;
2154 // move affected bookmarks down
2155 auto py = lc.pos2line(pos);
2156 if (pos != lc.line2pos(py)) ++py; // not the whole first line was modified, don't touch bookmarks on it
2157 // build new bookmark array
2158 int* newbm = cast(int*)malloc(int.sizeof*linebookmarked.length);
2159 if (newbm !is null) {
2160 scope(exit) free(newbm);
2161 int newbmpos = 0;
2162 bool smthWasChanged = false;
2163 foreach (int lidx; linebookmarked.byKey) {
2164 // fix bookmark line if necessary
2165 if (lidx >= py) { smthWasChanged = true; lidx += eolcount; }
2166 if (lidx < 0 || lidx >= lc.linecount) continue;
2167 //assert(lidx >= 0 && lidx < gb.linecount);
2168 // add this bookmark to new list
2169 newbm[newbmpos++] = lidx;
2171 // rebuild list if something was changed
2172 if (smthWasChanged) {
2173 fullDirty(); //TODO: optimize this
2174 linebookmarked.clear;
2175 foreach (int lidx; newbm[0..newbmpos]) linebookmarked[lidx] = true;
2177 } else {
2178 // out of memory, what to do? just clear bookmarks for now
2179 linebookmarked.clear;
2180 fullDirty(); // just in case
2185 public:
2187 this (int x0, int y0, int w, int h, EditorHL ahl=null, bool asingleline=false) {
2188 if (w < 2) w = 2;
2189 if (h < 1) h = 1;
2190 winx = x0;
2191 winy = y0;
2192 winw = w;
2193 winh = h;
2194 //setDirtyLinesLength(visibleLinesPerWindow);
2195 gb = new GapBuffer(asingleline);
2196 lc = new LineCache(gb);
2197 lc.recode1byte = &recode1b;
2198 hl = ahl;
2199 if (ahl !is null) { hl.gb = gb; hl.lc = lc; }
2200 undo = new UndoStack(mAsRich, false, !asingleline);
2201 redo = new UndoStack(mAsRich, true, !asingleline);
2202 mSingleLine = asingleline;
2205 private void setDirtyLinesLength (usize len) nothrow {
2206 if (len > int.max/4) assert(0, "wtf?!");
2207 if (dirtyLines.length > len) {
2208 dirtyLines.length = len;
2209 dirtyLines.assumeSafeAppend;
2210 dirtyLines[] = -1;
2211 } else if (dirtyLines.length < len) {
2212 auto optr = dirtyLines.ptr;
2213 auto olen = dirtyLines.length;
2214 dirtyLines.length = len;
2215 if (dirtyLines.ptr !is optr) {
2216 import core.memory : GC;
2217 if (dirtyLines.ptr is GC.addrOf(dirtyLines.ptr)) GC.setAttr(dirtyLines.ptr, GC.BlkAttr.NO_INTERIOR);
2219 //dirtyLines[olen..$] = -1;
2220 dirtyLines[] = -1;
2224 // utfuck switch hooks
2225 protected void beforeUtfuckSwitch (bool newisutfuck) {} /// utfuck switch hook
2226 protected void afterUtfuckSwitch (bool newisutfuck) {} /// utfuck switch hook
2228 final @property {
2230 bool utfuck () const pure nothrow @safe @nogc { pragma(inline, true); return lc.utfuck; }
2232 /// this switches "utfuck" mode
2233 /// note that utfuck mode is FUCKIN' SLOW and buggy
2234 /// you should not lose any text, but may encounter visual and positional glitches
2235 void utfuck (bool v) {
2236 if (lc.utfuck == v) return;
2237 beforeUtfuckSwitch(v);
2238 auto pos = curpos;
2239 lc.utfuck = v;
2240 lc.pos2xy(pos, cx, cy);
2241 fullDirty();
2242 afterUtfuckSwitch(v);
2245 ref inout(GapBuffer.HighState) defaultRichStyle () inout pure nothrow @trusted @nogc { pragma(inline, true); return cast(typeof(return))gb.defhs; } ///
2247 @property bool asRich () const pure nothrow @safe @nogc { pragma(inline, true); return mAsRich; } ///
2249 /// WARNING! changing this will reset undo/redo buffers!
2250 void asRich (bool v) {
2251 if (mAsRich != v) {
2252 // detach highlighter for "rich mode"
2253 if (v && hl !is null) {
2254 hl.gb = null;
2255 hl.lc = null;
2256 hl = null;
2258 mAsRich = v;
2259 if (undo !is null) {
2260 delete undo;
2261 undo = new UndoStack(mAsRich, false, !singleline);
2263 if (redo !is null) {
2264 delete redo;
2265 redo = new UndoStack(mAsRich, true, !singleline);
2267 gb.hasHiBuffer = v; // "rich" mode require highlighting buffer, normal mode doesn't, as it has no highlighter
2268 if (v && !gb.hasHiBuffer) assert(0, "out of memory"); // alas
2272 @property bool hasHiBuffer () const pure nothrow @safe @nogc { pragma(inline, true); return gb.hasHiBuffer; }
2273 @property void hasHiBuffer (bool v) nothrow @trusted @nogc {
2274 if (mAsRich) return; // cannot change
2275 if (hl !is null) return; // cannot change too
2276 gb.hasHiBuffer = v; // otherwise it is ok to change it
2279 int x0 () const pure nothrow @safe @nogc { pragma(inline, true); return winx; } ///
2280 int y0 () const pure nothrow @safe @nogc { pragma(inline, true); return winy; } ///
2281 int width () const pure nothrow @safe @nogc { pragma(inline, true); return winw; } ///
2282 int height () const pure nothrow @safe @nogc { pragma(inline, true); return winh; } ///
2284 void x0 (int v) { pragma(inline, true); move(v, winy); } ///
2285 void y0 (int v) { pragma(inline, true); move(winx, v); } ///
2286 void width (int v) { pragma(inline, true); resize(v, winh); } ///
2287 void height (int v) { pragma(inline, true); resize(winw, v); } ///
2289 /// has any effect only if you are using `insertText()` and `deleteText()` API!
2290 bool readonly () const pure nothrow @safe @nogc { pragma(inline, true); return mReadOnly; }
2291 void readonly (bool v) nothrow { pragma(inline, true); mReadOnly = v; } ///
2293 /// "single line" mode, for line editors
2294 bool singleline () const pure nothrow @safe @nogc { pragma(inline, true); return mSingleLine; }
2296 /// "buffer change counter"
2297 uint bufferCC () const pure nothrow @safe @nogc { pragma(inline, true); return gb.bufferChangeCounter; }
2298 void bufferCC (uint v) pure nothrow { pragma(inline, true); gb.bufferChangeCounter = v; } ///
2300 bool killTextOnChar () const pure nothrow @safe @nogc { pragma(inline, true); return mKillTextOnChar; } ///
2301 void killTextOnChar (bool v) nothrow { ///
2302 pragma(inline, true);
2303 if (mKillTextOnChar != v) {
2304 mKillTextOnChar = v;
2305 fullDirty();
2309 bool inPixels () const pure nothrow @safe @nogc { pragma(inline, true); return (lineHeightPixels != 0); } ///
2311 /// this can recalc height cache
2312 int linesPerWindow () nothrow {
2313 pragma(inline, true);
2314 return
2315 lineHeightPixels == 0 || lineHeightPixels == 1 ? winh :
2316 lineHeightPixels > 0 ? (winh <= lineHeightPixels ? 1 : winh/lineHeightPixels) :
2317 calcLinesPerWindow();
2320 /// this can recalc height cache
2321 int visibleLinesPerWindow () nothrow {
2322 pragma(inline, true);
2323 return
2324 lineHeightPixels == 0 || lineHeightPixels == 1 ? winh :
2325 lineHeightPixels > 0 ? (winh <= lineHeightPixels ? 1 : winh/lineHeightPixels+(winh%lineHeightPixels ? 1 : 0)) :
2326 calcVisLinesPerWindow();
2330 // for variable line height
2331 protected final int calcVisLinesPerWindow () nothrow {
2332 if (textMeter is null) assert(0, "you forgot to setup `textMeter` for EditorEngine");
2333 int hgtleft = winh;
2334 if (hgtleft < 1) return 1; // just in case
2335 int lidx = mTopLine;
2336 int lcount = 0;
2337 while (hgtleft > 0) {
2338 auto lh = lc.lineHeightPixels(lidx++);
2339 ++lcount;
2340 hgtleft -= lh;
2342 return lcount;
2345 // for variable line height
2346 protected final int calcLinesPerWindow () nothrow {
2347 if (textMeter is null) assert(0, "you forgot to setup `textMeter` for EditorEngine");
2348 int hgtleft = winh;
2349 if (hgtleft < 1) return 1; // just in case
2350 int lidx = mTopLine;
2351 int lcount = 0;
2352 //{ import core.stdc.stdio; printf("=== clpw ===\n"); }
2353 for (;;) {
2354 auto lh = lc.lineHeightPixels(lidx++);
2355 //if (gb.mLineCount > 0) { import core.stdc.stdio; printf("*clpw: lidx=%d; height=%d; hgtleft=%d\n", lidx-1, lh, hgtleft); }
2356 hgtleft -= lh;
2357 if (hgtleft >= 0) ++lcount;
2358 if (hgtleft <= 0) break;
2360 //{ import core.stdc.stdio; printf("clpw: %d\n", lcount); }
2361 return (lcount ? lcount : 1);
2364 /// has lille sense if `inPixels` is false
2365 final int linePixelHeight (int lidx) nothrow {
2366 if (!inPixels) return 1;
2367 if (lineHeightPixels > 0) {
2368 return lineHeightPixels;
2369 } else {
2370 if (textMeter is null) assert(0, "you forgot to setup `textMeter` for EditorEngine");
2371 return lc.lineHeightPixels(lidx);
2375 /// resize control
2376 void resize (int nw, int nh) {
2377 if (nw < 2) nw = 2;
2378 if (nh < 1) nh = 1;
2379 if (nw != winw || nh != winh) {
2380 winw = nw;
2381 winh = nh;
2382 auto nvl = visibleLinesPerWindow;
2383 setDirtyLinesLength(nvl);
2384 makeCurLineVisible();
2385 fullDirty();
2389 /// move control
2390 void move (int nx, int ny) {
2391 if (winx != nx || winy != ny) {
2392 winx = nx;
2393 winy = ny;
2394 fullDirty();
2398 /// move and resize control
2399 void moveResize (int nx, int ny, int nw, int nh) {
2400 move(nx, ny);
2401 resize(nw, nh);
2404 final @property void curx (int v) nothrow @system { gotoXY(v, cy); } ///
2405 final @property void cury (int v) nothrow @system { gotoXY(cx, v); } ///
2407 final @property nothrow {
2408 /// has active marked block?
2409 bool hasMarkedBlock () const pure @safe @nogc { pragma(inline, true); return (bstart < bend); }
2411 int curx () const pure @safe @nogc { pragma(inline, true); return cx; } ///
2412 int cury () const pure @safe @nogc { pragma(inline, true); return cy; } ///
2413 int xofs () const pure @safe @nogc { pragma(inline, true); return mXOfs; } ///
2415 int topline () const pure @safe @nogc { pragma(inline, true); return mTopLine; } ///
2416 int linecount () const pure @safe @nogc { pragma(inline, true); return lc.linecount; } ///
2417 int textsize () const pure @safe @nogc { pragma(inline, true); return gb.textsize; } ///
2419 char opIndex (int pos) const pure @safe @nogc { pragma(inline, true); return gb[pos]; } /// returns '\n' for out-of-bounds query
2421 /// returns '\n' for out-of-bounds query
2422 dchar dcharAt (int pos) const {
2423 auto ts = gb.textsize;
2424 if (pos < 0 || pos >= ts) return '\n';
2425 if (!lc.utfuck) {
2426 final switch (codepage) {
2427 case CodePage.koi8u: return koi2uni(gb[pos]);
2428 case CodePage.cp1251: return cp12512uni(gb[pos]);
2429 case CodePage.cp866: return cp8662uni(gb[pos]);
2431 assert(0);
2433 Utf8DecoderFast udc;
2434 while (pos < ts) {
2435 if (udc.decodeSafe(cast(ubyte)gb[pos++])) return cast(dchar)udc.codepoint;
2437 return udc.replacement;
2440 /// this advances `pos`, and returns '\n' for out-of-bounds query
2441 dchar dcharAtAdvance (ref int pos) const {
2442 auto ts = gb.textsize;
2443 if (pos < 0) { pos = 0; return '\n'; }
2444 if (pos >= ts) { pos = ts; return '\n'; }
2445 if (!lc.utfuck) {
2446 immutable char ch = gb[pos++];
2447 final switch (codepage) {
2448 case CodePage.koi8u: return koi2uni(ch);
2449 case CodePage.cp1251: return cp12512uni(ch);
2450 case CodePage.cp866: return cp8662uni(ch);
2452 assert(0);
2454 Utf8DecoderFast udc;
2455 while (pos < ts) {
2456 if (udc.decodeSafe(cast(ubyte)gb[pos++])) return cast(dchar)udc.codepoint;
2458 return udc.replacement;
2461 /// this works correctly with utfuck
2462 int nextpos (int pos) const {
2463 if (pos < 0) return 0;
2464 immutable ts = gb.textsize;
2465 if (pos >= ts) return ts;
2466 if (!lc.utfuck) return pos+1;
2467 Utf8DecoderFast udc;
2468 while (pos < ts) if (udc.decodeSafe(cast(ubyte)gb[pos++])) break;
2469 return pos;
2472 /// this sometimes works correctly with utfuck
2473 int prevpos (int pos) const {
2474 if (pos <= 0) return 0;
2475 immutable ts = gb.textsize;
2476 if (ts == 0) return 0;
2477 if (pos > ts) pos = ts;
2478 --pos;
2479 if (lc.utfuck) {
2480 while (pos > 0 && !isValidUtf8Start(cast(ubyte)gb[pos])) --pos;
2482 return pos;
2485 bool textChanged () const pure { pragma(inline, true); return txchanged; } ///
2486 void textChanged (bool v) pure { pragma(inline, true); txchanged = v; } ///
2488 bool visualtabs () const pure { pragma(inline, true); return (lc.visualtabs && lc.tabsize > 0); } ///
2491 void visualtabs (bool v) {
2492 if (lc.visualtabs != v) {
2493 lc.visualtabs = v;
2494 fullDirty();
2498 ubyte tabsize () const pure { pragma(inline, true); return lc.tabsize; } ///
2501 void tabsize (ubyte v) {
2502 if (lc.tabsize != v) {
2503 lc.tabsize = v;
2504 if (lc.visualtabs) fullDirty();
2508 /// mark whole visible text as dirty
2509 final void fullDirty () nothrow { dirtyLines[] = -1; }
2513 @property void topline (int v) nothrow {
2514 if (v < 0) v = 0;
2515 if (v > lc.linecount) v = lc.linecount-1;
2516 immutable auto moldtop = mTopLine;
2517 mTopLine = v; // for linesPerWindow
2518 if (v+linesPerWindow > lc.linecount) {
2519 v = lc.linecount-linesPerWindow;
2520 if (v < 0) v = 0;
2522 if (v != moldtop) {
2523 mTopLine = moldtop;
2524 version(egeditor_record_movement_undo) pushUndoCurPos();
2525 mTopLine = v;
2529 /// absolute coordinates in text
2530 final void gotoXY(bool vcenter=false) (int nx, int ny) nothrow {
2531 if (nx < 0) nx = 0;
2532 if (ny < 0) ny = 0;
2533 if (ny >= lc.linecount) ny = lc.linecount-1;
2534 auto pos = lc.xy2pos(nx, ny);
2535 lc.pos2xy(pos, nx, ny);
2536 if (nx != cx || ny != cy) {
2537 version(egeditor_record_movement_undo) pushUndoCurPos();
2538 cx = nx;
2539 cy = ny;
2540 static if (vcenter) makeCurLineVisibleCentered(); else makeCurLineVisible();
2545 final void gotoPos(bool vcenter=false) (int pos) nothrow {
2546 if (pos < 0) pos = 0;
2547 if (pos > gb.textsize) pos = gb.textsize;
2548 int rx, ry;
2549 lc.pos2xy(pos, rx, ry);
2550 gotoXY!vcenter(rx, ry);
2553 final int curpos () nothrow { pragma(inline, true); return lc.xy2pos(cx, cy); } ///
2554 final void curpos (int pos) nothrow { pragma(inline, true); gotoPos(pos); } ///
2556 /// get text coordinates for the given position
2557 void pos2xy (int pos, out int x, out int y) nothrow { pragma(inline, true); lc.pos2xy(pos, out x, out y); }
2559 /// get position for the given text coordinates
2560 int xy2pos (int x, int y) nothrow { pragma(inline, true); return lc.xy2pos(x, y); }
2563 void clearUndo () nothrow {
2564 if (undo !is null) undo.clear();
2565 if (redo !is null) redo.clear();
2569 void clear () nothrow {
2570 lc.clear();
2571 txchanged = false;
2572 if (undo !is null) undo.clear();
2573 if (redo !is null) redo.clear();
2574 cx = cy = mTopLine = mXOfs = 0;
2575 prevTopLine = -1;
2576 prevXOfs = -1;
2577 dirtyLines[] = -1;
2578 bstart = bend = -1;
2579 markingBlock = false;
2580 lastBGEnd = false;
2581 txchanged = false;
2585 void clearAndDisableUndo () {
2586 if (undo !is null) delete undo;
2587 if (redo !is null) delete redo;
2591 void reinstantiateUndo () {
2592 if (undo is null) undo = new UndoStack(mAsRich, false, !mSingleLine);
2593 if (redo is null) redo = new UndoStack(mAsRich, true, !mSingleLine);
2597 void loadFile (const(char)[] fname) { loadFile(VFile(fname)); }
2600 void loadFile (VFile fl) {
2601 import core.stdc.stdlib : malloc, free;
2602 clear();
2603 scope(failure) clear();
2604 auto fpos = fl.tell;
2605 auto fsz = fl.size;
2606 if (fpos < fsz) {
2607 if (fsz-fpos >= gb.tbmax) throw new Exception("text too big");
2608 uint filesize = cast(uint)(fsz-fpos);
2609 if (!lc.resizeBuffer(filesize)) throw new Exception("text too big");
2610 scope(failure) clear();
2611 fl.rawReadExact(lc.getBufferPtr[]);
2612 if (!lc.rebuild()) throw new Exception("out of memory");
2617 void saveFile (const(char)[] fname) { saveFile(VFile(fname, "w")); }
2620 void saveFile (VFile fl) {
2621 gb.forEachBufPart(0, gb.textsize, delegate (const(char)[] buf) { fl.rawWriteExact(buf); });
2622 txchanged = false;
2623 if (undo !is null) undo.alwaysChanged();
2624 if (redo !is null) redo.alwaysChanged();
2627 /// attach new highlighter; return previous one
2628 /// note that you can't reuse one highlighter for several editors!
2629 EditorHL attachHiglighter (EditorHL ahl) {
2630 if (mAsRich) { assert(hl is null); return null; } // oops
2631 if (ahl is hl) return ahl; // nothing to do
2632 EditorHL prevhl = hl;
2633 if (ahl is null) {
2634 // detach
2635 if (hl !is null) {
2636 hl.gb = null;
2637 hl.lc = null;
2638 hl = null;
2639 gb.hasHiBuffer = false; // don't need it
2640 fullDirty();
2642 return prevhl; // return previous
2644 if (ahl.lc !is null) {
2645 if (ahl.lc !is lc) throw new Exception("highlighter already used by another editor");
2646 if (ahl !is hl) assert(0, "something is VERY wrong");
2647 return ahl;
2649 if (hl !is null) { hl.gb = null; hl.lc = null; }
2650 ahl.gb = gb;
2651 ahl.lc = lc;
2652 hl = ahl;
2653 gb.hasHiBuffer = true; // need it
2654 if (!gb.hasHiBuffer) assert(0, "out of memory"); // alas
2655 ahl.lineChanged(0, true);
2656 fullDirty();
2657 return prevhl;
2661 EditorHL detachHighlighter () {
2662 if (mAsRich) { assert(hl is null); return null; } // oops
2663 auto res = hl;
2664 if (res !is null) {
2665 hl.gb = null;
2666 hl.lc = null;
2667 hl = null;
2668 gb.hasHiBuffer = false; // don't need it
2669 fullDirty();
2671 return res;
2674 /// override this method to draw something before any other page drawing will be done
2675 public void drawPageBegin () {}
2677 /// override this method to draw one text line
2678 /// highlighting is done, other housekeeping is done, only draw
2679 /// lidx is always valid
2680 /// must repaint the whole line
2681 /// use `winXXX` vars to know window dimensions
2682 public abstract void drawLine (int lidx, int yofs, int xskip);
2684 /// just clear the line; you have to override this, 'cause it is used to clear empty space
2685 /// use `winXXX` vars to know window dimensions
2686 public abstract void drawEmptyLine (int yofs);
2688 /// override this method to draw something after page was drawn, but before drawing the status
2689 public void drawPageMisc () {}
2691 /// override this method to draw status line; it will be called after `drawPageBegin()`
2692 public void drawStatus () {}
2694 /// override this method to draw something after status was drawn, but before drawing the cursor
2695 public void drawPagePost () {}
2697 /// override this method to draw text cursor; it will be called after `drawPageMisc()`
2698 public abstract void drawCursor ();
2700 /// override this method to draw something (or flush drawing buffer) after everything was drawn
2701 public void drawPageEnd () {}
2703 /** draw the page; it will fix coords, call necessary methods and so on. you are usually don't need to override this.
2704 * page drawing flow:
2705 * drawPageBegin();
2706 * page itself with drawLine() or drawEmptyLine();
2707 * drawPageMisc();
2708 * drawStatus();
2709 * drawPagePost();
2710 * drawCursor();
2711 * drawPageEnd();
2713 void drawPage () {
2714 makeCurLineVisible();
2716 if (prevTopLine != mTopLine || prevXOfs != mXOfs) {
2717 prevTopLine = mTopLine;
2718 prevXOfs = mXOfs;
2719 dirtyLines[] = -1;
2722 drawPageBegin();
2723 immutable int lhp = lineHeightPixels;
2724 immutable int ydelta = (inPixels ? lhp : 1);
2725 bool alwaysDirty = false;
2726 auto pos = lc.xy2pos(0, mTopLine);
2727 auto lc = lc.linecount;
2728 int lyofs = 0;
2729 //TODO: optimize redrawing for variable line height mode
2730 foreach (int y; 0..visibleLinesPerWindow) {
2731 bool dirty = (mTopLine+y < lc && hl !is null && hl.fixLine(mTopLine+y));
2732 if (!alwaysDirty) {
2733 if (lhp < 0) {
2734 // variable line height, hacks
2735 alwaysDirty = (!alwaysDirty && y < dirtyLines.length ? (dirtyLines.ptr[y] != linePixelHeight(mTopLine+y)) : true);
2736 } else if (!dirty && y < dirtyLines.length) {
2737 // tty or constant pixel height
2738 dirty = (dirtyLines.ptr[y] != 0);
2740 dirty = true;
2742 if (dirty || alwaysDirty) {
2743 if (y < dirtyLines.length) dirtyLines.ptr[y] = (lhp >= 0 ? 0 : linePixelHeight(mTopLine+y));
2744 if (mTopLine+y < lc) {
2745 drawLine(mTopLine+y, lyofs, mXOfs);
2746 } else {
2747 drawEmptyLine(lyofs);
2750 lyofs += (ydelta > 0 ? ydelta : linePixelHeight(mTopLine+y));
2752 drawPageMisc();
2753 drawStatus();
2754 drawPagePost();
2755 drawCursor();
2756 drawPageEnd();
2759 /// force cursor coordinates to be in text
2760 final void normXY () nothrow {
2761 lc.pos2xy(curpos, cx, cy);
2765 final void makeCurXVisible () nothrow {
2766 // use "real" x coordinate to calculate x offset
2767 if (cx < 0) cx = 0;
2768 int rx;
2769 if (!inPixels) {
2770 int ry;
2771 lc.pos2xyVT(curpos, rx, ry);
2772 if (rx < mXOfs) mXOfs = rx;
2773 if (rx-mXOfs >= winw) mXOfs = rx-winw+1;
2774 } else {
2775 rx = localCursorX();
2776 rx += mXOfs;
2777 if (rx < mXOfs) mXOfs = rx-8;
2778 if (rx+4-mXOfs > winw) mXOfs = rx-winw+4;
2780 if (mXOfs < 0) mXOfs = 0;
2783 /// in symbols, not chars
2784 final int linelen (int lidx) nothrow {
2785 if (lidx < 0 || lidx >= lc.linecount) return 0;
2786 auto pos = lc.line2pos(lidx);
2787 auto ts = gb.textsize;
2788 if (pos > ts) pos = ts;
2789 int res = 0;
2790 if (!lc.utfuck) {
2791 if (mSingleLine) return ts-pos;
2792 while (pos < ts) {
2793 if (gb[pos++] == '\n') break;
2794 ++res;
2796 } else {
2797 immutable bool sl = mSingleLine;
2798 while (pos < ts) {
2799 char ch = gb[pos++];
2800 if (!sl && ch == '\n') break;
2801 ++res;
2802 if (ch >= 128) {
2803 --pos;
2804 pos += gb.utfuckLenAt(pos);
2808 return res;
2811 /// cursor position in "local" coords: from widget (x0,y0), possibly in pixels
2812 final int localCursorX () nothrow {
2813 int rx;
2814 localCursorXY(&rx, null);
2815 return rx;
2818 /// cursor position in "local" coords: from widget (x0,y0), possibly in pixels
2819 final void localCursorXY (int* lcx, int* lcy) nothrow {
2820 int rx, ry;
2821 if (!inPixels) {
2822 lc.pos2xyVT(curpos, rx, ry);
2823 ry -= mTopLine;
2824 rx -= mXOfs;
2825 if (lcx !is null) *lcx = rx;
2826 if (lcy !is null) *lcy = ry;
2827 } else {
2828 lc.pos2xy(curpos, rx, ry);
2829 if (textMeter is null) assert(0, "you forgot to setup `textMeter` for EditorEngine");
2830 if (lcy !is null) {
2831 if (lineHeightPixels > 0) {
2832 *lcy = (ry-mTopLine)*lineHeightPixels;
2833 } else {
2834 if (ry >= mTopLine) {
2835 for (int ll = mTopLine; ll < ry; ++ll) *lcy += lc.lineHeightPixels(ll);
2836 } else {
2837 for (int ll = mTopLine-1; ll >= ry; --ll) *lcy -= lc.lineHeightPixels(ll);
2841 if (rx == 0) { if (lcx !is null) *lcx = 0-mXOfs; return; }
2842 if (lcx !is null) {
2843 textMeter.reset(visualtabs ? lc.tabsize : 0);
2844 scope(exit) textMeter.finish(); // just in case
2845 auto pos = lc.linestart(ry);
2846 immutable int le = lc.linestart(ry+1);
2847 immutable bool ufuck = lc.utfuck;
2848 if (mSingleLine) {
2849 while (pos < le) {
2850 // advance one symbol
2851 textMeter.advance(dcharAtAdvance(pos), gb.hi(pos));
2852 --rx;
2853 if (rx == 0) break;
2855 } else {
2856 while (pos < le) {
2857 // advance one symbol
2858 if (gb[pos] == '\n') break;
2859 textMeter.advance(dcharAtAdvance(pos), gb.hi(pos));
2860 --rx;
2861 if (rx == 0) {
2862 // hack for kerning
2863 if (gb[pos] != '\n' && pos < le) {
2864 textMeter.advance(dcharAtAdvance(pos), gb.hi(pos));
2865 *lcx = textMeter.currofs;
2866 return;
2868 break;
2872 *lcx = textMeter.currwdt-mXOfs;
2877 /// convert coordinates in widget into text coordinates; can be used to convert mouse click position into text position
2878 /// WARNING: ty can be equal to linecount or -1!
2879 final void widget2text (int mx, int my, out int tx, out int ty) nothrow {
2880 if (!inPixels) {
2881 int ry = my+mTopLine;
2882 if (ry < 0) { ty = -1; return; } // tx is zero here
2883 if (ry >= lc.linecount) { ty = lc.linecount; return; } // tx is zero here
2884 if (mx <= 0 && mXOfs == 0) return; // tx is zero here
2885 // ah, screw it! user should not call this very often, so i can stop caring about speed.
2886 int visx = -mXOfs;
2887 auto pos = lc.line2pos(ry);
2888 auto ts = gb.textsize;
2889 int rx = 0;
2890 immutable bool ufuck = lc.utfuck;
2891 immutable bool sl = mSingleLine;
2892 while (pos < ts) {
2893 // advance one symbol
2894 char ch = gb[pos];
2895 if (!sl && ch == '\n') { tx = rx; return; } // done anyway
2896 int nextx = visx+1;
2897 if (ch == '\t' && visualtabs) {
2898 // hack!
2899 nextx = ((visx+mXOfs)/tabsize+1)*tabsize-mXOfs;
2901 if (mx >= visx && mx < nextx) { tx = rx; return; }
2902 visx = nextx;
2903 if (!ufuck || ch < 128) {
2904 ++pos;
2905 } else {
2906 pos = nextpos(pos);
2908 ++rx;
2910 } else {
2911 if (textMeter is null) assert(0, "you forgot to setup `textMeter` for EditorEngine");
2912 int ry;
2913 if (lineHeightPixels > 0) {
2914 ry = my/lineHeightPixels+mTopLine;
2915 } else {
2916 ry = mTopLine;
2917 if (my >= 0) {
2918 // down
2919 int lcy = 0;
2920 while (lcy < my) {
2921 lcy += lc.lineHeightPixels(ry);
2922 if (lcy > my) break;
2923 ++ry;
2924 if (lcy == my) break;
2926 } else {
2927 // up
2928 ry = mTopLine-1;
2929 int lcy = 0;
2930 while (ry >= 0) {
2931 int upy = lcy-lc.lineHeightPixels(ry);
2932 if (my >= upy && my < lcy) break;
2933 lcy = upy;
2937 if (ry < 0) { ty = -1; return; } // tx is zero here
2938 if (ry >= lc.linecount) { ty = lc.linecount; return; } // tx is zero here
2939 ty = ry;
2940 if (mx <= 0 && mXOfs == 0) return; // tx is zero here
2941 // now the hard part
2942 textMeter.reset(visualtabs ? lc.tabsize : 0);
2943 scope(exit) textMeter.finish(); // just in case
2944 int visx0 = -mXOfs;
2945 auto pos = lc.line2pos(ry);
2946 immutable ts = gb.textsize;
2947 int rx = 0;
2948 immutable bool ufuck = lc.utfuck;
2949 immutable bool sl = mSingleLine;
2950 while (pos < ts) {
2951 // advance one symbol
2952 char ch = gb[pos];
2953 if (!sl && ch == '\n') { tx = rx; return; } // done anyway
2954 if (!ufuck || ch < 128) {
2955 textMeter.advance(cast(dchar)ch, gb.hi(pos));
2956 ++pos;
2957 } else {
2958 textMeter.advance(dcharAtAdvance(pos), gb.hi(pos));
2960 immutable int visx1 = textMeter.currwdt-mXOfs;
2961 // visx0 is current char x start
2962 // visx1 is current char x end
2963 // so if our mx is in [visx0..visx1), we are at current char
2964 if (mx >= visx0 && mx < visx1) {
2965 // it is more natural this way
2966 if (mx >= visx0+(visx1-visx0)/2 && pos < lc.linestart(ry+1)) ++rx;
2967 tx = rx;
2968 return;
2970 ++rx;
2971 visx0 = visx1;
2977 final void makeCurLineVisible () nothrow {
2978 if (cy < 0) cy = 0;
2979 if (cy >= lc.linecount) cy = lc.linecount-1;
2980 if (cy < mTopLine) {
2981 mTopLine = cy;
2982 } else {
2983 if (cy > mTopLine+linesPerWindow-1) {
2984 mTopLine = cy-linesPerWindow+1;
2985 if (mTopLine < 0) mTopLine = 0;
2988 setDirtyLinesLength(visibleLinesPerWindow);
2989 makeCurXVisible();
2993 final void makeCurLineVisibleCentered (bool forced=false) nothrow {
2994 if (forced || !isCurLineVisible) {
2995 if (cy < 0) cy = 0;
2996 if (cy >= lc.linecount) cy = lc.linecount-1;
2997 mTopLine = cy-linesPerWindow/2;
2998 if (mTopLine < 0) mTopLine = 0;
2999 if (mTopLine+linesPerWindow > lc.linecount) {
3000 mTopLine = lc.linecount-linesPerWindow;
3001 if (mTopLine < 0) mTopLine = 0;
3004 setDirtyLinesLength(visibleLinesPerWindow);
3005 makeCurXVisible();
3009 final bool isCurLineBeforeTop () nothrow {
3010 pragma(inline, true);
3011 return (cy < mTopLine);
3015 final bool isCurLineAfterBottom () nothrow {
3016 pragma(inline, true);
3017 return (cy > mTopLine+linesPerWindow-1);
3021 final bool isCurLineVisible () nothrow {
3022 pragma(inline, true);
3023 return (cy >= mTopLine && cy < mTopLine+linesPerWindow);
3024 //if (cy < mTopLine) return false;
3025 //if (cy > mTopLine+linesPerWindow-1) return false;
3026 //return true;
3029 /// `updateDown`: update all the page (as new lines was inserted/removed)
3030 final void lineChanged (int lidx, bool updateDown) {
3031 if (lidx < 0 || lidx >= lc.linecount) return;
3032 if (hl !is null) hl.lineChanged(lidx, updateDown);
3033 if (lidx < mTopLine) { if (updateDown) dirtyLines[] = -1; return; }
3034 if (lidx >= mTopLine+linesPerWindow) return;
3035 immutable stl = lidx-mTopLine;
3036 assert(stl >= 0);
3037 if (stl < dirtyLines.length) {
3038 if (updateDown) {
3039 dirtyLines[stl..$] = -1;
3040 } else {
3041 dirtyLines.ptr[stl] = -1;
3047 final void lineChangedByPos (int pos, bool updateDown) { return lineChanged(lc.pos2line(pos), updateDown); }
3050 final void markLinesDirty (int lidx, int count) nothrow {
3051 if (prevTopLine != mTopLine || prevXOfs != mXOfs) return; // we will refresh the whole page anyway
3052 if (count < 1 || lidx >= lc.linecount) return;
3053 if (count > lc.linecount) count = lc.linecount;
3054 if (lidx >= mTopLine+linesPerWindow) return;
3055 int le = lidx+count;
3056 if (le <= mTopLine) { dirtyLines[] = -1; return; } // just in case
3057 if (lidx < mTopLine) { dirtyLines[] = -1; lidx = mTopLine; return; } // just in cale
3058 if (le > mTopLine+visibleLinesPerWindow) le = mTopLine+visibleLinesPerWindow;
3059 immutable stl = lidx-mTopLine;
3060 assert(stl >= 0);
3061 if (stl < dirtyLines.length) {
3062 auto el = le-mTopLine;
3063 if (el > dirtyLines.length) el = cast(int)dirtyLines.length;
3064 dirtyLines.ptr[stl..el] = -1;
3069 final void markLinesDirtySE (int lidxs, int lidxe) nothrow {
3070 if (lidxe < lidxs) { int tmp = lidxs; lidxs = lidxe; lidxe = tmp; }
3071 markLinesDirty(lidxs, lidxe-lidxs+1);
3075 final void markRangeDirty (int pos, int len) nothrow {
3076 if (prevTopLine != mTopLine || prevXOfs != mXOfs) return; // we will refresh the whole page anyway
3077 int l0 = lc.pos2line(pos);
3078 int l1 = lc.pos2line(pos+len+1);
3079 markLinesDirtySE(l0, l1);
3083 final void markBlockDirty () nothrow {
3084 //FIXME: optimize updating with block boundaries
3085 if (bstart >= bend) return;
3086 markRangeDirty(bstart, bend-bstart);
3089 /// do various fixups before text deletion
3090 /// cursor coords *may* be already changed
3091 /// will be called before text deletion by `deleteText` or `replaceText` APIs
3092 /// eolcount: number of eols in (to be) deleted block
3093 protected void willBeDeleted (int pos, int len, int eolcount) nothrow {
3094 //FIXME: optimize updating with block boundaries
3095 if (len < 1) return; // just in case
3096 assert(pos >= 0 && cast(long)pos+len <= gb.textsize);
3097 bookmarkDeletionFix(pos, len, eolcount);
3098 if (hasMarkedBlock) {
3099 if (pos+len <= bstart) {
3100 // move whole block up
3101 markBlockDirty();
3102 bstart -= len;
3103 bend -= len;
3104 markBlockDirty();
3105 lastBGEnd = false;
3106 } else if (pos <= bstart && pos+len >= bend) {
3107 // whole block will be deleted
3108 doBlockResetMark(false); // skip undo
3109 } else if (pos >= bstart && pos+len <= bend) {
3110 // deleting something inside block, move end
3111 markBlockDirty();
3112 bend -= len;
3113 if (bstart >= bend) {
3114 doBlockResetMark(false); // skip undo
3115 } else {
3116 markBlockDirty();
3117 lastBGEnd = true;
3119 } else if (pos >= bstart && pos < bend && pos+len > bend) {
3120 // chopping block end
3121 markBlockDirty();
3122 bend = pos;
3123 if (bstart >= bend) {
3124 doBlockResetMark(false); // skip undo
3125 } else {
3126 markBlockDirty();
3127 lastBGEnd = true;
3133 /// do various fixups after text deletion
3134 /// cursor coords *may* be already changed
3135 /// will be called after text deletion by `deleteText` or `replaceText` APIs
3136 /// eolcount: number of eols in deleted block
3137 /// pos and len: they were valid *before* deletion!
3138 protected void wasDeleted (int pos, int len, int eolcount) nothrow {
3141 /// do various fixups before text insertion
3142 /// cursor coords *may* be already changed
3143 /// will be called before text insertion by `insertText` or `replaceText` APIs
3144 /// eolcount: number of eols in (to be) inserted block
3145 protected void willBeInserted (int pos, int len, int eolcount) nothrow {
3148 /// do various fixups after text insertion
3149 /// cursor coords *may* be already changed
3150 /// will be called after text insertion by `insertText` or `replaceText` APIs
3151 /// eolcount: number of eols in inserted block
3152 protected void wasInserted (int pos, int len, int eolcount) nothrow {
3153 //FIXME: optimize updating with block boundaries
3154 if (len < 1) return;
3155 assert(pos >= 0 && cast(long)pos+len <= gb.textsize);
3156 bookmarkInsertionFix(pos, len, eolcount);
3157 if (markingBlock && pos == bend) {
3158 bend += len;
3159 markBlockDirty();
3160 lastBGEnd = true;
3161 return;
3163 if (hasMarkedBlock) {
3164 if (pos <= bstart) {
3165 // move whole block down
3166 markBlockDirty();
3167 bstart += len;
3168 bend += len;
3169 markBlockDirty();
3170 lastBGEnd = false;
3171 } else if (pos < bend) {
3172 // move end of block down
3173 markBlockDirty();
3174 bend += len;
3175 markBlockDirty();
3176 lastBGEnd = true;
3181 /// should be called after cursor position change
3182 protected final void growBlockMark () nothrow {
3183 if (!markingBlock || bstart < 0) return;
3184 makeCurLineVisible();
3185 int ry;
3186 int pos = curpos;
3187 if (pos < bstart) {
3188 if (lastBGEnd) {
3189 // move end
3190 ry = lc.pos2line(bend);
3191 bend = bstart;
3192 bstart = pos;
3193 lastBGEnd = false;
3194 } else {
3195 // move start
3196 ry = lc.pos2line(bstart);
3197 if (bstart == pos) return;
3198 bstart = pos;
3199 lastBGEnd = false;
3201 } else if (pos > bend) {
3202 // move end
3203 if (bend == pos) return;
3204 ry = lc.pos2line(bend-1);
3205 bend = pos;
3206 lastBGEnd = true;
3207 } else if (pos >= bstart && pos < bend) {
3208 // shrink block
3209 if (lastBGEnd) {
3210 // from end
3211 if (bend == pos) return;
3212 ry = lc.pos2line(bend-1);
3213 bend = pos;
3214 } else {
3215 // from start
3216 if (bstart == pos) return;
3217 ry = lc.pos2line(bstart);
3218 bstart = pos;
3221 markLinesDirtySE(ry, cy);
3224 /// all the following text operations will be grouped into one undo action
3225 bool undoGroupStart () {
3226 return (undo !is null ? undo.addGroupStart(this) : false);
3229 /// end undo action started with `undoGroupStart()`
3230 bool undoGroupEnd () {
3231 return (undo !is null ? undo.addGroupEnd(this) : false);
3234 /// build autoindent for the current line, put it into `indentText`
3235 /// `indentText` will include '\n'
3236 protected final void buildIndent (int pos) {
3237 if (indentText.length) { indentText.length = 0; indentText.assumeSafeAppend; }
3238 void putToIT (char ch) {
3239 auto optr = indentText.ptr;
3240 indentText ~= ch;
3241 if (optr !is indentText.ptr) {
3242 import core.memory : GC;
3243 if (indentText.ptr is GC.addrOf(indentText.ptr)) {
3244 GC.setAttr(indentText.ptr, GC.BlkAttr.NO_INTERIOR); // less false positives
3248 putToIT('\n');
3249 pos = lc.line2pos(lc.pos2line(pos));
3250 auto ts = gb.textsize;
3251 int curx = 0;
3252 while (pos < ts) {
3253 if (curx == cx) break;
3254 auto ch = gb[pos];
3255 if (ch == '\n') break;
3256 if (ch > ' ') break;
3257 putToIT(ch);
3258 ++pos;
3259 ++curx;
3263 /// delete text, save undo, mark updated lines
3264 /// return `false` if operation cannot be performed
3265 /// if caller wants to delete more text than buffer has, it is ok
3266 /// calls `dg` *after* undo saving, but before `willBeDeleted()`
3267 final bool deleteText(string movecursor="none") (int pos, int count, scope void delegate (int pos, int count) dg=null) {
3268 static assert(movecursor == "none" || movecursor == "start" || movecursor == "end");
3269 if (mReadOnly) return false;
3270 killTextOnChar = false;
3271 auto ts = gb.textsize;
3272 if (pos < 0 || pos >= ts || count < 0) return false;
3273 if (ts-pos < count) count = ts-pos;
3274 if (count > 0) {
3275 bool undoOk = false;
3276 if (undo !is null) undoOk = undo.addTextRemove(this, pos, count);
3277 if (dg !is null) dg(pos, count);
3278 int delEols = (!mSingleLine ? gb.countEolsInRange(pos, count) : 0);
3279 willBeDeleted(pos, count, delEols);
3280 //writeLogAction(pos, -count);
3281 // hack: if new linecount is different, there was '\n' in text
3282 auto olc = lc.linecount;
3283 if (!lc.remove(pos, count)) {
3284 if (undoOk) undo.popUndo(); // remove undo record
3285 return false;
3287 txchanged = true;
3288 static if (movecursor != "none") lc.pos2xy(pos, cx, cy);
3289 wasDeleted(pos, count, delEols);
3290 lineChangedByPos(pos, (lc.linecount != olc));
3291 } else {
3292 static if (movecursor != "none") {
3293 int rx, ry;
3294 lc.pos2xy(curpos, rx, ry);
3295 if (rx != cx || ry != cy) {
3296 version(egeditor_record_movement_undo) {
3297 immutable bool okmove = pushUndoCurPos();
3298 } else {
3299 enum okmove = true;
3301 if (okmove) {
3302 cx = rx;
3303 cy = ry;
3304 markLinesDirty(cy, 1);
3309 return true;
3312 /// ugly name is intentional
3313 /// this replaces editor text, clears undo and sets `killTextOnChar` if necessary
3314 /// it also ignores "readonly" flag
3315 final bool setNewText (const(char)[] text, bool killOnChar=true) {
3316 auto oldro = mReadOnly;
3317 scope(exit) mReadOnly = oldro;
3318 mReadOnly = false;
3319 clear();
3320 auto res = insertText!"end"(0, text);
3321 clearUndo();
3322 if (mSingleLine) killTextOnChar = killOnChar;
3323 fullDirty();
3324 return res;
3327 /// insert text, save undo, mark updated lines
3328 /// return `false` if operation cannot be performed
3329 final bool insertText(string movecursor="none", bool doIndent=true) (int pos, const(char)[] str) {
3330 static assert(movecursor == "none" || movecursor == "start" || movecursor == "end");
3331 if (mReadOnly) return false;
3332 if (mKillTextOnChar) {
3333 killTextOnChar = false;
3334 if (gb.textsize > 0) {
3335 undoGroupStart();
3336 bstart = bend = -1;
3337 markingBlock = false;
3338 deleteText!"start"(0, gb.textsize);
3339 undoGroupEnd();
3342 auto ts = gb.textsize;
3343 if (pos < 0 || str.length >= int.max/3) return false;
3344 if (pos > ts) pos = ts;
3345 if (str.length > 0) {
3346 int nlc = (!mSingleLine ? GapBuffer.countEols(str) : 0);
3347 static if (doIndent) {
3348 if (nlc) {
3349 // want indenting and has at least one newline, hard case
3350 buildIndent(pos);
3351 if (indentText.length) {
3352 int toinsert = cast(int)str.length+nlc*(cast(int)indentText.length-1);
3353 bool undoOk = false;
3354 bool doRollback = false;
3355 // record undo
3356 if (undo !is null) undoOk = undo.addTextInsert(this, pos, toinsert);
3357 willBeInserted(pos, toinsert, nlc);
3358 auto spos = pos;
3359 auto ipos = pos;
3360 while (str.length > 0) {
3361 int elp = GapBuffer.findEol(str);
3362 if (elp < 0) elp = cast(int)str.length;
3363 if (elp > 0) {
3364 // insert text
3365 auto newpos = lc.put(ipos, str[0..elp]);
3366 if (newpos < 0) { doRollback = true; break; }
3367 ipos = newpos;
3368 str = str[elp..$];
3369 } else {
3370 // insert newline
3371 assert(str[0] == '\n');
3372 auto newpos = lc.put(ipos, indentText);
3373 if (newpos < 0) { doRollback = true; break; }
3374 ipos = newpos;
3375 str = str[1..$];
3378 if (doRollback) {
3379 // operation failed, rollback it
3380 if (ipos > spos) lc.remove(spos, ipos-spos); // remove inserted text
3381 if (undoOk) undo.popUndo(); // remove undo record
3382 return false;
3384 //if (ipos-spos != toinsert) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "spos=%d; ipos=%d; ipos-spos=%d; toinsert=%d; nlc=%d; sl=%d; il=%d\n", spos, ipos, ipos-spos, toinsert, nlc, cast(int)str.length, cast(int)indentText.length); }
3385 assert(ipos-spos == toinsert);
3386 static if (movecursor == "start") lc.pos2xy(spos, cx, cy);
3387 else static if (movecursor == "end") lc.pos2xy(ipos, cx, cy);
3388 txchanged = true;
3389 lineChangedByPos(spos, true);
3390 wasInserted(spos, toinsert, nlc);
3391 return true;
3395 // either we don't want indenting, or there are no eols in new text
3397 bool undoOk = false;
3398 // record undo
3399 if (undo !is null) undoOk = undo.addTextInsert(this, pos, cast(int)str.length);
3400 willBeInserted(pos, cast(int)str.length, nlc);
3401 // insert text
3402 auto newpos = lc.put(pos, str[]);
3403 if (newpos < 0) {
3404 // operation failed, rollback it
3405 if (undoOk) undo.popUndo(); // remove undo record
3406 return false;
3408 static if (movecursor == "start") lc.pos2xy(pos, cx, cy);
3409 else static if (movecursor == "end") lc.pos2xy(newpos, cx, cy);
3410 txchanged = true;
3411 lineChangedByPos(pos, (nlc > 0));
3412 wasInserted(pos, newpos-pos, nlc);
3415 return true;
3418 /// replace text at pos, save undo, mark updated lines
3419 /// return `false` if operation cannot be performed
3420 final bool replaceText(string movecursor="none", bool doIndent=false) (int pos, int count, const(char)[] str) {
3421 static assert(movecursor == "none" || movecursor == "start" || movecursor == "end");
3422 if (mReadOnly) return false;
3423 if (count < 0 || pos < 0) return false;
3424 if (mKillTextOnChar) {
3425 killTextOnChar = false;
3426 if (gb.textsize > 0) {
3427 undoGroupStart();
3428 bstart = bend = -1;
3429 markingBlock = false;
3430 deleteText!"start"(0, gb.textsize);
3431 undoGroupEnd();
3434 auto ts = gb.textsize;
3435 if (pos >= ts) pos = ts;
3436 if (count > ts-pos) count = ts-pos;
3437 bool needToRestoreBlock = (markingBlock || hasMarkedBlock);
3438 auto bs = bstart;
3439 auto be = bend;
3440 auto mb = markingBlock;
3441 undoGroupStart();
3442 scope(exit) undoGroupEnd();
3443 auto ocp = curpos;
3444 deleteText!movecursor(pos, count);
3445 static if (movecursor == "none") { bool cmoved = false; if (ocp > pos) { cmoved = true; ocp -= count; } }
3446 if (insertText!(movecursor, doIndent)(pos, str)) {
3447 static if (movecursor == "none") { if (cmoved) ocp += count; }
3448 if (needToRestoreBlock && !hasMarkedBlock) {
3449 // restore block if it was deleted
3450 bstart = bs;
3451 bend = be-count+cast(int)str.length;
3452 markingBlock = mb;
3453 if (bend < bstart) markingBlock = false;
3454 lastBGEnd = true;
3455 } else if (hasMarkedBlock && bs == pos && bstart > pos) {
3456 // consider the case when replaced text is inside the block,
3457 // and block is starting on the text
3458 bstart = pos;
3459 lastBGEnd = false; //???
3460 markBlockDirty();
3462 return true;
3464 return false;
3468 bool doBlockWrite (const(char)[] fname) {
3469 killTextOnChar = false;
3470 if (!hasMarkedBlock) return true;
3471 if (bend-bstart <= 0) return true;
3472 return doBlockWrite(VFile(fname, "w"));
3476 bool doBlockWrite (VFile fl) {
3477 import core.stdc.stdlib : malloc, free;
3478 killTextOnChar = false;
3479 if (!hasMarkedBlock) return true;
3480 gb.forEachBufPart(bstart, bend-bstart, delegate (const(char)[] buf) { fl.rawWriteExact(buf); });
3481 return true;
3485 bool doBlockRead (const(char)[] fname) { return doBlockRead(VFile(fname)); }
3488 bool doBlockRead (VFile fl) {
3489 //FIXME: optimize this!
3490 import core.stdc.stdlib : realloc, free;
3491 import core.stdc.string : memcpy;
3492 // read block data into temp buffer
3493 if (mReadOnly) return false;
3494 killTextOnChar = false;
3495 char* btext;
3496 scope(exit) if (btext !is null) free(btext);
3497 int blen = 0;
3498 char[1024] tb = void;
3499 for (;;) {
3500 auto rd = fl.rawRead(tb[]);
3501 if (rd.length == 0) break;
3502 if (blen+rd.length > int.max/2) return false;
3503 auto nb = cast(char*)realloc(btext, blen+rd.length);
3504 if (nb is null) return false;
3505 btext = nb;
3506 memcpy(btext+blen, rd.ptr, rd.length);
3507 blen += cast(int)rd.length;
3509 return insertText!("start", false)(curpos, btext[0..blen]); // no indent
3513 bool doBlockDelete () {
3514 if (mReadOnly) return false;
3515 if (!hasMarkedBlock) return true;
3516 return deleteText!"start"(bstart, bend-bstart, (pos, count) { doBlockResetMark(false); });
3520 bool doBlockCopy () {
3521 //FIXME: optimize this!
3522 import core.stdc.stdlib : malloc, free;
3523 if (mReadOnly) return false;
3524 killTextOnChar = false;
3525 if (!hasMarkedBlock) return true;
3526 // copy block data into temp buffer
3527 int blen = bend-bstart;
3528 GapBuffer.HighState* hsbuf;
3529 scope(exit) if (hsbuf !is null) free(hsbuf);
3530 if (asRich) {
3531 // rich text: get atts
3532 hsbuf = cast(GapBuffer.HighState*)malloc(blen*hsbuf[0].sizeof);
3533 if (hsbuf is null) return false;
3534 foreach (int pp; bstart..bend) hsbuf[pp-bstart] = gb.hi(pp);
3536 // normal text
3537 char* btext = cast(char*)malloc(blen);
3538 if (btext is null) return false; // alas
3539 scope(exit) free(btext);
3540 foreach (int pp; bstart..bend) btext[pp-bstart] = gb[pp];
3541 auto stp = curpos;
3542 return insertText!("start", false)(stp, btext[0..blen]); // no indent
3543 // attrs
3544 if (asRich) {
3545 foreach (immutable int idx; 0..blen) gb.hi(stp+idx) = hsbuf[idx];
3550 bool doBlockMove () {
3551 //FIXME: optimize this!
3552 import core.stdc.stdlib : malloc, free;
3553 if (mReadOnly) return false;
3554 killTextOnChar = false;
3555 if (!hasMarkedBlock) return true;
3556 int pos = curpos;
3557 if (pos >= bstart && pos < bend) return false; // can't do this while we are inside the block
3558 // copy block data into temp buffer
3559 int blen = bend-bstart;
3560 GapBuffer.HighState* hsbuf;
3561 scope(exit) if (hsbuf !is null) free(hsbuf);
3562 if (asRich) {
3563 // rich text: get atts
3564 hsbuf = cast(GapBuffer.HighState*)malloc(blen*hsbuf[0].sizeof);
3565 if (hsbuf is null) return false;
3566 foreach (int pp; bstart..bend) hsbuf[pp-bstart] = gb.hi(pp);
3568 char* btext = cast(char*)malloc(blen);
3569 if (btext is null) return false; // alas
3570 scope(exit) free(btext);
3571 foreach (int pp; bstart..bend) btext[pp-bstart] = gb[pp];
3572 // group undo action
3573 bool undoOk = undoGroupStart();
3574 if (pos >= bstart) pos -= blen;
3575 if (!doBlockDelete()) {
3576 // rollback
3577 if (undoOk) undo.popUndo();
3578 return false;
3580 auto stp = pos;
3581 if (!insertText!("start", false)(pos, btext[0..blen])) {
3582 // rollback
3583 if (undoOk) undo.popUndo();
3584 return false;
3586 // attrs
3587 if (asRich) {
3588 foreach (immutable int idx; 0..blen) gb.hi(stp+idx) = hsbuf[idx];
3590 // mark moved block
3591 bstart = pos;
3592 bend = pos+blen;
3593 markBlockDirty();
3594 undoGroupEnd();
3595 return true;
3599 void doDelete () {
3600 if (mReadOnly) return;
3601 int pos = curpos;
3602 if (pos >= gb.textsize) return;
3603 if (!lc.utfuck) {
3604 deleteText!"start"(pos, 1);
3605 } else {
3606 deleteText!"start"(pos, gb.utfuckLenAt(pos));
3611 void doBackspace () {
3612 if (mReadOnly) return;
3613 killTextOnChar = false;
3614 int pos = curpos;
3615 if (pos == 0) return;
3616 immutable int ppos = prevpos(pos);
3617 deleteText!"start"(ppos, pos-ppos);
3621 void doBackByIndent () {
3622 if (mReadOnly) return;
3623 int pos = curpos;
3624 int ls = lc.xy2pos(0, cy);
3625 if (pos == ls) { doDeleteWord(); return; }
3626 if (gb[pos-1] > ' ') { doDeleteWord(); return; }
3627 int rx, ry;
3628 lc.pos2xy(pos, rx, ry);
3629 int del = 2-rx%2;
3630 if (del > 1 && (pos-2 < ls || gb[pos-2] > ' ')) del = 1;
3631 pos -= del;
3632 deleteText!"start"(pos, del);
3636 void doDeleteWord () {
3637 if (mReadOnly) return;
3638 int pos = curpos;
3639 if (pos == 0) return;
3640 auto ch = gb[pos-1];
3641 if (!mSingleLine && ch == '\n') { doBackspace(); return; }
3642 int stpos = pos-1;
3643 // find word start
3644 if (ch <= ' ') {
3645 while (stpos > 0) {
3646 ch = gb[stpos-1];
3647 if ((!mSingleLine && ch == '\n') || ch > ' ') break;
3648 --stpos;
3650 } else if (isWordChar(ch)) {
3651 while (stpos > 0) {
3652 ch = gb[stpos-1];
3653 if (!isWordChar(ch)) break;
3654 --stpos;
3657 if (pos == stpos) return;
3658 deleteText!"start"(stpos, pos-stpos);
3662 void doKillLine () {
3663 if (mReadOnly) return;
3664 int ls = lc.xy2pos(0, cy);
3665 int le;
3666 if (cy == lc.linecount-1) {
3667 le = gb.textsize;
3668 } else {
3669 le = lc.xy2pos(0, cy+1);
3671 if (ls < le) deleteText!"start"(ls, le-ls);
3675 void doKillToEOL () {
3676 if (mReadOnly) return;
3677 int pos = curpos;
3678 auto ts = gb.textsize;
3679 if (mSingleLine) {
3680 if (pos < ts) deleteText!"start"(pos, ts-pos);
3681 } else {
3682 if (pos < ts && gb[pos] != '\n') {
3683 int epos = pos+1;
3684 while (epos < ts && gb[epos] != '\n') ++epos;
3685 deleteText!"start"(pos, epos-pos);
3690 /// split line at current position
3691 bool doLineSplit (bool autoindent=true) {
3692 if (mReadOnly || mSingleLine) return false;
3693 if (autoindent) {
3694 return insertText!("end", true)(curpos, "\n");
3695 } else {
3696 return insertText!("end", false)(curpos, "\n");
3700 /// put char in koi8
3701 void doPutChar (char ch) {
3702 if (mReadOnly) return;
3703 if (!mSingleLine && ch == '\n') { doLineSplit(inPasteMode <= 0); return; }
3704 if (ch > 127 && lc.utfuck) {
3705 char[8] ubuf = void;
3706 int len = utf8Encode(ubuf[], koi2uni(ch));
3707 if (len < 1) { ubuf[0] = '?'; len = 1; }
3708 insertText!("end", true)(curpos, ubuf[0..len]);
3709 return;
3711 if (ch >= 128 && codepage != CodePage.koi8u) ch = recodeCharTo(ch);
3712 if (inPasteMode <= 0) {
3713 insertText!("end", true)(curpos, (&ch)[0..1]);
3714 } else {
3715 insertText!("end", false)(curpos, (&ch)[0..1]);
3720 void doPutDChar (dchar dch) {
3721 if (mReadOnly) return;
3722 if (!Utf8DecoderFast.isValidDC(dch)) dch = Utf8DecoderFast.replacement;
3723 if (dch < 128) { doPutChar(cast(char)dch); return; }
3724 char[4] ubuf = void;
3725 auto len = utf8Encode(ubuf[], dch);
3726 if (len < 1) return;
3727 if (lc.utfuck) {
3728 insertText!"end"(curpos, ubuf.ptr[0..len]);
3729 } else {
3730 // recode to codepage
3731 doPutChar(recodeU2B(dch));
3736 void doPutTextUtf (const(char)[] str) {
3737 if (mReadOnly) return;
3738 if (str.length == 0) return;
3740 if (!mSingleLine && utfuck && inPasteMode > 0) {
3741 insertText!"end"(curpos, str);
3742 return;
3745 bool ugstarted = false;
3746 void startug () { if (!ugstarted) { ugstarted = true; undoGroupStart(); } }
3747 scope(exit) if (ugstarted) undoGroupEnd();
3749 Utf8DecoderFast udc;
3750 foreach (immutable char ch; str) {
3751 if (udc.decodeSafe(cast(ubyte)ch)) {
3752 dchar dch = cast(dchar)udc.codepoint;
3753 if (!mSingleLine && dch == '\n') { startug(); doLineSplit(inPasteMode <= 0); continue; }
3754 startug();
3755 doPutChar(recodeU2B(dch));
3760 /// put text in koi8
3761 void doPutText (const(char)[] str) {
3762 if (mReadOnly) return;
3763 if (str.length == 0) return;
3765 if (!mSingleLine && !utfuck && codepage == CodePage.koi8u && inPasteMode > 0) {
3766 insertText!"end"(curpos, str);
3767 return;
3770 bool ugstarted = false;
3771 void startug () { if (!ugstarted) { ugstarted = true; undoGroupStart(); } }
3772 scope(exit) if (ugstarted) undoGroupEnd();
3774 usize pos = 0;
3775 char ch;
3776 while (pos < str.length) {
3777 auto stpos = pos;
3778 while (pos < str.length) {
3779 ch = str.ptr[pos];
3780 if (!mSingleLine && ch == '\n') break;
3781 if (ch >= 128) break;
3782 ++pos;
3784 if (stpos < pos) { startug(); insertText!"end"(curpos, str.ptr[stpos..pos]); }
3785 if (pos >= str.length) break;
3786 ch = str.ptr[pos];
3787 if (!mSingleLine && ch == '\n') { startug(); doLineSplit(inPasteMode <= 0); ++pos; continue; }
3788 if (ch < ' ') { startug(); insertText!"end"(curpos, str.ptr[pos..pos+1]); ++pos; continue; }
3789 Utf8DecoderFast udc;
3790 stpos = pos;
3791 while (pos < str.length) if (udc.decode(cast(ubyte)(str.ptr[pos++]))) break;
3792 startug();
3793 if (udc.complete) {
3794 insertText!"end"(curpos, str.ptr[stpos..pos]);
3795 } else {
3796 ch = uni2koi(Utf8DecoderFast.replacement);
3797 insertText!"end"(curpos, (&ch)[0..1]);
3803 void doPasteStart () {
3804 if (mKillTextOnChar) {
3805 killTextOnChar = false;
3806 if (gb.textsize > 0) {
3807 undoGroupStart();
3808 bstart = bend = -1;
3809 markingBlock = false;
3810 deleteText!"start"(0, gb.textsize);
3811 undoGroupEnd();
3814 undoGroupStart();
3815 ++inPasteMode;
3819 void doPasteEnd () {
3820 killTextOnChar = false;
3821 if (--inPasteMode < 0) inPasteMode = 0;
3822 undoGroupEnd();
3825 protected final bool xIndentLine (int lidx) {
3826 //TODO: rollback
3827 if (mReadOnly) return false;
3828 if (lidx < 0 || lidx >= lc.linecount) return false;
3829 auto pos = lc.xy2pos(0, lidx);
3830 auto epos = lc.xy2pos(0, lidx+1);
3831 auto stpos = pos;
3832 // if line consists of blanks only, don't do anything
3833 while (pos < epos) {
3834 auto ch = gb[pos];
3835 if (ch == '\n') return true;
3836 if (ch > ' ') break;
3837 ++pos;
3839 if (pos >= gb.textsize) return true;
3840 pos = stpos;
3841 char[2] spc = ' ';
3842 return insertText!("none", false)(pos, spc[]);
3846 void doIndentBlock () {
3847 if (mReadOnly) return;
3848 killTextOnChar = false;
3849 if (!hasMarkedBlock) return;
3850 int sy = lc.pos2line(bstart);
3851 int ey = lc.pos2line(bend-1);
3852 bool bsAtBOL = (bstart == lc.line2pos(sy));
3853 undoGroupStart();
3854 scope(exit) undoGroupEnd();
3855 foreach (int lidx; sy..ey+1) xIndentLine(lidx);
3856 if (bsAtBOL) bstart = lc.line2pos(sy); // line already marked as dirty
3859 protected final bool xUnindentLine (int lidx) {
3860 if (mReadOnly) return false;
3861 if (lidx < 0 || lidx >= lc.linecount) return true;
3862 auto pos = lc.xy2pos(0, lidx);
3863 auto len = 1;
3864 if (gb[pos] > ' ' || gb[pos] == '\n') return true;
3865 if (pos+1 < gb.textsize && gb[pos+1] <= ' ' && gb[pos+1] != '\n') ++len;
3866 return deleteText!"none"(pos, len);
3870 void doUnindentBlock () {
3871 if (mReadOnly) return;
3872 killTextOnChar = false;
3873 if (!hasMarkedBlock) return;
3874 int sy = lc.pos2line(bstart);
3875 int ey = lc.pos2line(bend-1);
3876 undoGroupStart();
3877 scope(exit) undoGroupEnd();
3878 foreach (int lidx; sy..ey+1) xUnindentLine(lidx);
3881 // ////////////////////////////////////////////////////////////////////// //
3882 // actions
3884 version(egeditor_record_movement_undo) {
3885 /// push cursor position to undo stack
3886 final bool pushUndoCurPos () nothrow {
3887 return (undo !is null ? undo.addCurMove(this) : false);
3891 // returns old state
3892 enum SetupShiftMarkingMixin = q{
3893 auto omb = markingBlock;
3894 scope(exit) markingBlock = omb;
3895 if (domark) {
3896 if (!hasMarkedBlock) {
3897 int pos = curpos;
3898 bstart = bend = pos;
3899 lastBGEnd = true;
3901 markingBlock = true;
3906 void doWordLeft (bool domark=false) {
3907 mixin(SetupShiftMarkingMixin);
3908 killTextOnChar = false;
3909 int pos = curpos;
3910 if (pos == 0) return;
3911 auto ch = gb[pos-1];
3912 if (!mSingleLine && ch == '\n') { doLeft(); return; }
3913 int stpos = pos-1;
3914 // find word start
3915 if (ch <= ' ') {
3916 while (stpos > 0) {
3917 ch = gb[stpos-1];
3918 if ((!mSingleLine && ch == '\n') || ch > ' ') break;
3919 --stpos;
3922 if (stpos > 0 && isWordChar(ch)) {
3923 while (stpos > 0) {
3924 ch = gb[stpos-1];
3925 if (!isWordChar(ch)) break;
3926 --stpos;
3929 version(egeditor_record_movement_undo) pushUndoCurPos();
3930 lc.pos2xy(stpos, cx, cy);
3931 growBlockMark();
3935 void doWordRight (bool domark=false) {
3936 mixin(SetupShiftMarkingMixin);
3937 killTextOnChar = false;
3938 int pos = curpos;
3939 if (pos == gb.textsize) return;
3940 auto ch = gb[pos];
3941 if (!mSingleLine && ch == '\n') { doRight(); return; }
3942 int epos = pos+1;
3943 // find word start
3944 if (ch <= ' ') {
3945 while (epos < gb.textsize) {
3946 ch = gb[epos];
3947 if ((!mSingleLine && ch == '\n') || ch > ' ') break;
3948 ++epos;
3950 } else if (isWordChar(ch)) {
3951 while (epos < gb.textsize) {
3952 ch = gb[epos];
3953 if (!isWordChar(ch)) {
3954 if (ch <= ' ') {
3955 while (epos < gb.textsize) {
3956 ch = gb[epos];
3957 if ((!mSingleLine && ch == '\n') || ch > ' ') break;
3958 ++epos;
3961 break;
3963 ++epos;
3966 version(egeditor_record_movement_undo) pushUndoCurPos();
3967 lc.pos2xy(epos, cx, cy);
3968 growBlockMark();
3972 void doTextTop (bool domark=false) {
3973 mixin(SetupShiftMarkingMixin);
3974 if (lc.mLineCount < 2) return;
3975 killTextOnChar = false;
3976 if (mTopLine == 0 && cy == 0) return;
3977 version(egeditor_record_movement_undo) pushUndoCurPos();
3978 mTopLine = cy = 0;
3979 growBlockMark();
3983 void doTextBottom (bool domark=false) {
3984 mixin(SetupShiftMarkingMixin);
3985 if (lc.mLineCount < 2) return;
3986 killTextOnChar = false;
3987 if (cy >= lc.linecount-1) return;
3988 version(egeditor_record_movement_undo) pushUndoCurPos();
3989 cy = lc.linecount-1;
3990 growBlockMark();
3994 void doPageTop (bool domark=false) {
3995 mixin(SetupShiftMarkingMixin);
3996 if (lc.mLineCount < 2) return;
3997 killTextOnChar = false;
3998 if (cy == mTopLine) return;
3999 version(egeditor_record_movement_undo) pushUndoCurPos();
4000 cy = mTopLine;
4001 growBlockMark();
4005 void doPageBottom (bool domark=false) {
4006 mixin(SetupShiftMarkingMixin);
4007 if (lc.mLineCount < 2) return;
4008 killTextOnChar = false;
4009 int ny = mTopLine+linesPerWindow-1;
4010 if (ny >= lc.linecount) ny = lc.linecount-1;
4011 if (cy != ny) {
4012 version(egeditor_record_movement_undo) pushUndoCurPos();
4013 cy = ny;
4015 growBlockMark();
4019 void doScrollUp (bool domark=false) {
4020 mixin(SetupShiftMarkingMixin);
4021 if (mTopLine > 0) {
4022 killTextOnChar = false;
4023 version(egeditor_record_movement_undo) pushUndoCurPos();
4024 --mTopLine;
4025 --cy;
4026 } else if (cy > 0) {
4027 killTextOnChar = false;
4028 version(egeditor_record_movement_undo) pushUndoCurPos();
4029 --cy;
4031 growBlockMark();
4035 void doScrollDown (bool domark=false) {
4036 mixin(SetupShiftMarkingMixin);
4037 if (mTopLine+linesPerWindow < lc.linecount) {
4038 killTextOnChar = false;
4039 version(egeditor_record_movement_undo) pushUndoCurPos();
4040 ++mTopLine;
4041 ++cy;
4042 } else if (cy < lc.linecount-1) {
4043 killTextOnChar = false;
4044 version(egeditor_record_movement_undo) pushUndoCurPos();
4045 ++cy;
4047 growBlockMark();
4051 void doUp (bool domark=false) {
4052 mixin(SetupShiftMarkingMixin);
4053 if (cy > 0) {
4054 killTextOnChar = false;
4055 version(egeditor_record_movement_undo) pushUndoCurPos();
4056 --cy;
4057 // visjump
4058 if (winh >= 24 && isCurLineBeforeTop) {
4059 mTopLine = cy-(winh/3);
4060 if (mTopLine < 0) mTopLine = 0;
4061 setDirtyLinesLength(visibleLinesPerWindow);
4062 makeCurXVisible();
4065 growBlockMark();
4069 void doDown (bool domark=false) {
4070 mixin(SetupShiftMarkingMixin);
4071 if (cy < lc.linecount-1) {
4072 killTextOnChar = false;
4073 version(egeditor_record_movement_undo) pushUndoCurPos();
4074 ++cy;
4075 // visjump
4076 if (winh >= 24 && isCurLineAfterBottom) {
4077 mTopLine = cy+(winh/3)-linesPerWindow+1;
4078 if (mTopLine < 0) mTopLine = 0;
4079 setDirtyLinesLength(visibleLinesPerWindow);
4080 makeCurXVisible();
4083 growBlockMark();
4087 void doLeft (bool domark=false) {
4088 mixin(SetupShiftMarkingMixin);
4089 int rx, ry;
4090 killTextOnChar = false;
4091 lc.pos2xy(curpos, rx, ry);
4092 if (cx > rx) cx = rx;
4093 if (cx > 0) {
4094 version(egeditor_record_movement_undo) pushUndoCurPos();
4095 --cx;
4096 } else if (cy > 0) {
4097 // to prev line
4098 version(egeditor_record_movement_undo) pushUndoCurPos();
4099 lc.pos2xy(lc.xy2pos(0, cy)-1, cx, cy);
4101 growBlockMark();
4105 void doRight (bool domark=false) {
4106 mixin(SetupShiftMarkingMixin);
4107 int rx, ry;
4108 killTextOnChar = false;
4109 lc.pos2xy(lc.xy2pos(cx+1, cy), rx, ry);
4110 if (cx+1 > rx) {
4111 if (cy < lc.linecount-1) {
4112 version(egeditor_record_movement_undo) pushUndoCurPos();
4113 cx = 0;
4114 ++cy;
4116 } else {
4117 version(egeditor_record_movement_undo) pushUndoCurPos();
4118 ++cx;
4120 growBlockMark();
4124 void doPageUp (bool domark=false) {
4125 mixin(SetupShiftMarkingMixin);
4126 if (linesPerWindow < 2 || lc.mLineCount < 2) return;
4127 killTextOnChar = false;
4128 int ntl = mTopLine-(linesPerWindow-1);
4129 int ncy = cy-(linesPerWindow-1);
4130 if (ntl < 0) ntl = 0;
4131 if (ncy < 0) ncy = 0;
4132 if (ntl != mTopLine || ncy != cy) {
4133 version(egeditor_record_movement_undo) pushUndoCurPos();
4134 mTopLine = ntl;
4135 cy = ncy;
4137 growBlockMark();
4141 void doPageDown (bool domark=false) {
4142 mixin(SetupShiftMarkingMixin);
4143 if (linesPerWindow < 2 || lc.mLineCount < 2) return;
4144 killTextOnChar = false;
4145 int ntl = mTopLine+(linesPerWindow-1);
4146 int ncy = cy+(linesPerWindow-1);
4147 if (ntl+linesPerWindow >= lc.linecount) ntl = lc.linecount-linesPerWindow;
4148 if (ncy >= lc.linecount) ncy = lc.linecount-1;
4149 if (ntl < 0) ntl = 0;
4150 if (ntl != mTopLine || ncy != cy) {
4151 version(egeditor_record_movement_undo) pushUndoCurPos();
4152 mTopLine = ntl;
4153 cy = ncy;
4155 growBlockMark();
4159 void doHome (bool smart=true, bool domark=false) {
4160 mixin(SetupShiftMarkingMixin);
4161 killTextOnChar = false;
4162 if (cx != 0) {
4163 version(egeditor_record_movement_undo) pushUndoCurPos();
4164 cx = 0;
4165 } else {
4166 if (!smart) return;
4167 int nx = 0;
4168 auto pos = lc.xy2pos(0, cy);
4169 while (pos < gb.textsize) {
4170 auto ch = gb[pos];
4171 if (!mSingleLine && ch == '\n') return;
4172 if (ch > ' ') break;
4173 ++pos;
4174 ++nx;
4176 if (nx != cx) {
4177 version(egeditor_record_movement_undo) pushUndoCurPos();
4178 cx = nx;
4181 growBlockMark();
4185 void doEnd (bool domark=false) {
4186 mixin(SetupShiftMarkingMixin);
4187 int rx, ry;
4188 killTextOnChar = false;
4189 int ep;
4190 if (cy >= lc.linecount-1) {
4191 ep = gb.textsize;
4192 } else {
4193 ep = lc.lineend(cy);
4194 //if (gb[ep] != '\n') ++ep; // word wrapping
4196 lc.pos2xy(ep, rx, ry);
4197 if (rx != cx || ry != cy) {
4198 version(egeditor_record_movement_undo) pushUndoCurPos();
4199 cx = rx;
4200 cy = ry;
4202 growBlockMark();
4206 /*private*/protected void doUndoRedo (UndoStack us) { // "allMembers" trait: shut the fuck up!
4207 if (us is null) return;
4208 killTextOnChar = false;
4209 int level = 0;
4210 while (us.hasUndo) {
4211 auto tp = us.undoAction(this);
4212 switch (tp) {
4213 case UndoStack.Type.GroupStart:
4214 if (--level <= 0) return;
4215 break;
4216 case UndoStack.Type.GroupEnd:
4217 ++level;
4218 break;
4219 default:
4220 if (level <= 0) return;
4221 break;
4226 void doUndo () { doUndoRedo(undo); } ///
4227 void doRedo () { doUndoRedo(redo); } ///
4230 void doBlockResetMark (bool saveUndo=true) nothrow {
4231 killTextOnChar = false;
4232 if (bstart < bend) {
4233 version(egeditor_record_movement_undo) if (saveUndo) pushUndoCurPos();
4234 markLinesDirtySE(lc.pos2line(bstart), lc.pos2line(bend-1));
4236 bstart = bend = -1;
4237 markingBlock = false;
4240 /// toggle block marking mode
4241 void doToggleBlockMarkMode () {
4242 killTextOnChar = false;
4243 if (bstart == bend && markingBlock) { doBlockResetMark(false); return; }
4244 if (bstart < bend && !markingBlock) doBlockResetMark(false);
4245 int pos = curpos;
4246 if (!hasMarkedBlock) {
4247 bstart = bend = pos;
4248 markingBlock = true;
4249 lastBGEnd = true;
4250 } else {
4251 if (pos != bstart) {
4252 bend = pos;
4253 if (bend < bstart) { pos = bstart; bstart = bend; bend = pos; }
4255 markingBlock = false;
4256 dirtyLines[] = -1; //FIXME: optimize
4261 void doSetBlockStart () {
4262 killTextOnChar = false;
4263 auto pos = curpos;
4264 if ((hasMarkedBlock || (bstart == bend && bstart >= 0 && bstart < gb.textsize)) && pos < bend) {
4265 //if (pos < bstart) markRangeDirty(pos, bstart-pos); else markRangeDirty(bstart, pos-bstart);
4266 bstart = pos;
4267 lastBGEnd = false;
4268 } else {
4269 doBlockResetMark();
4270 bstart = bend = pos;
4271 lastBGEnd = false;
4273 markingBlock = false;
4274 dirtyLines[] = -1; //FIXME: optimize
4278 void doSetBlockEnd () {
4279 auto pos = curpos;
4280 if ((hasMarkedBlock || (bstart == bend && bstart >= 0 && bstart < gb.textsize)) && pos > bstart) {
4281 //if (pos < bend) markRangeDirty(pos, bend-pos); else markRangeDirty(bend, pos-bend);
4282 bend = pos;
4283 lastBGEnd = true;
4284 } else {
4285 doBlockResetMark();
4286 bstart = bend = pos;
4287 lastBGEnd = true;
4289 markingBlock = false;
4290 dirtyLines[] = -1; //FIXME: optimize
4293 protected:
4294 // called by undo/redo processors
4295 final void ubTextRemove (int pos, int len) {
4296 if (mReadOnly) return;
4297 killTextOnChar = false;
4298 int nlc = (!mSingleLine ? gb.countEolsInRange(pos, len) : 0);
4299 bookmarkDeletionFix(pos, len, nlc);
4300 lineChangedByPos(pos, (nlc > 0));
4301 lc.remove(pos, len);
4304 // called by undo/redo processors
4305 final bool ubTextInsert (int pos, const(char)[] str) {
4306 if (mReadOnly) return true;
4307 killTextOnChar = false;
4308 if (str.length == 0) return true;
4309 int nlc = (!mSingleLine ? gb.countEols(str) : 0);
4310 bookmarkInsertionFix(pos, pos+cast(int)str.length, nlc);
4311 if (lc.put(pos, str) >= 0) {
4312 lineChangedByPos(pos, (nlc > 0));
4313 return true;
4314 } else {
4315 return false;
4319 // can be called only after `ubTextInsert`, and with the same pos/length
4320 // usually it is done by undo/redo action if the editor is in "rich mode"
4321 final void ubTextSetAttrs (int pos, const(GapBuffer.HighState)[] hs) {
4322 if (mReadOnly || hs.length == 0) return;
4323 assert(gb.hasHiBuffer);
4324 foreach (const ref hi; hs) gb.hbuf[gb.pos2real(pos++)] = hi;
4327 // ////////////////////////////////////////////////////////////////////// //
4328 public static struct TextRange {
4329 private:
4330 EditorEngine ed;
4331 int pos;
4332 int left; // chars left, including front
4333 char frontch = 0;
4334 nothrow:
4335 private:
4336 this (EditorEngine aed, int apos, int aleft, char afrontch) pure {
4337 ed = aed;
4338 pos = apos;
4339 left = aleft;
4340 frontch = afrontch;
4343 this (EditorEngine aed, usize lo, usize hi) {
4344 ed = aed;
4345 if (aed !is null && lo < hi && lo < aed.gb.textsize) {
4346 pos = cast(int)lo;
4347 if (hi > ed.gb.textsize) hi = ed.gb.textsize;
4348 left = cast(int)hi-pos+1; // compensate for first popFront
4349 popFront();
4352 public:
4353 @property bool empty () const pure @safe @nogc { pragma(inline, true); return (left <= 0); }
4354 @property char front () const pure @safe @nogc { pragma(inline, true); return frontch; }
4355 void popFront () {
4356 if (ed is null || left < 2) { left = 0; frontch = 0; return; }
4357 --left;
4358 if (pos >= ed.gb.textsize) { left = 0; frontch = 0; return; }
4359 frontch = ed.gb[pos++];
4361 auto save () pure { pragma(inline, true); return TextRange(ed, pos, left, frontch); }
4362 @property usize length () const pure @safe @nogc { pragma(inline, true); return (left > 0 ? left : 0); }
4363 alias opDollar = length;
4364 char opIndex (usize idx) {
4365 pragma(inline, true);
4366 return (left > 0 && idx < left ? (idx == 0 ? frontch : ed.gb[pos+cast(int)idx-1]) : 0);
4368 auto opSlice () pure { pragma(inline, true); return this.save; }
4369 //WARNING: logic untested!
4370 auto opSlice (uint lo, uint hi) {
4371 if (ed is null || left <= 0 || lo >= left || lo >= hi) return TextRange(null, 0, 0);
4372 hi -= lo; // convert to length
4373 if (hi > left) hi = left;
4374 if (left-lo > hi) hi = left-lo;
4375 return TextRange(ed, cast(int)lo+1, cast(int)hi, ed.gb[cast(int)lo]);
4377 // make it bidirectional, just for fun
4378 //WARNING: completely untested!
4379 char back () const pure {
4380 pragma(inline, true);
4381 return (ed !is null && left > 0 ? (left == 1 ? frontch : ed.gb[pos+left-2]) : 0);
4383 void popBack () {
4384 if (ed is null || left < 2) { left = 0; frontch = 0; return; }
4385 --left;
4389 public:
4390 /// range interface to editor text
4391 /// WARNING! do not change anything while range is active, or results *WILL* be UD
4392 final TextRange opSlice (usize lo, usize hi) nothrow { return TextRange(this, lo, hi); }
4393 final TextRange opSlice () nothrow { return TextRange(this, 0, gb.textsize); } /// ditto
4394 final int opDollar () nothrow { return gb.textsize; } ///
4397 final TextRange markedBlockRange () nothrow {
4398 if (!hasMarkedBlock) return TextRange.init;
4399 return TextRange(this, bstart, bend);
4402 static:
4404 bool isWordChar (char ch) pure nothrow {
4405 return (ch.isalnum || ch == '_' || ch > 127);