egra: draw ttf glyph with vertical gradient
[iv.d.git] / egeditor / editor.d
blobfde1082483ea2ce1860520d967fd53f52610a1f0
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;
21 import iv.alice;
22 import iv.strex;
23 import iv.utfutil;
24 import iv.vfs;
25 debug import iv.vfs.io;
27 version(egeditor_scan_time) import iv.pxclock;
30 // ////////////////////////////////////////////////////////////////////////// //
31 /// this interface is used to measure text for pixel-sized editor
32 abstract class EgTextMeter {
33 int currofs; /// x offset for current char (i.e. the last char that was passed to `advance()` should be drawn with this offset)
34 int currwdt; /// current line width (including, the last char that was passed to `advance()`), preferably without trailing empty space between chars
35 int currheight; /// current text height; keep this in sync with the current state; `reset` should set it to "default text height"
37 /// this should reset text width iterator (and curr* fields); tabsize > 0: process tabs as... well... tabs ;-); tabsize < 0: calculating height
38 abstract void reset (int tabsize) nothrow;
40 /// advance text width iterator, fix all curr* fields
41 abstract void advance (dchar ch, in ref GapBuffer.HighState hs) nothrow;
43 /// finish text iterator; it should NOT reset curr* fields!
44 /// WARNING: EditorEngine tries to call this after each `reset()`, but user code may not
45 abstract void finish () nothrow;
49 // ////////////////////////////////////////////////////////////////////////// //
50 /// highlighter should be able to work line-by-line
51 class EditorHL {
52 protected:
53 GapBuffer gb; /// this will be set by EditorEngine on attaching
54 LineCache lc; /// this will be set by EditorEngine on attaching
56 public:
57 this () {} ///
59 /// return true if highlighting for this line was changed
60 abstract bool fixLine (int line);
62 /// mark line as "need rehighlighting" (and possibly other text too)
63 /// wasInsDel: some lines was inserted/deleted down the text
64 abstract void lineChanged (int line, bool wasInsDel);
68 // ////////////////////////////////////////////////////////////////////////// //
69 ///
70 public final class GapBuffer {
71 public:
72 static align(1) struct HighState {
73 align(1):
74 ubyte kwtype; // keyword number
75 ubyte kwidx; // index in keyword
76 @property pure nothrow @safe @nogc {
77 ushort u16 () const { pragma(inline, true); return cast(ushort)((kwidx<<8)|kwtype); }
78 short s16 () const { pragma(inline, true); return cast(short)((kwidx<<8)|kwtype); }
79 void u16 (ushort v) { pragma(inline, true); kwtype = v&0xff; kwidx = (v>>8)&0xff; }
80 void s16 (short v) { pragma(inline, true); kwtype = v&0xff; kwidx = (v>>8)&0xff; }
84 private:
85 HighState hidummy;
86 bool mSingleLine;
88 protected:
89 enum MinGapSize = 1024; // bytes in gap
90 enum GrowGran = 65536; // must be power of 2
91 enum MinGapSizeSmall = 64; // bytes in gap
92 enum GrowGranSmall = 0x100; // must be power of 2
94 static assert(GrowGran >= MinGapSize);
95 static assert(GrowGranSmall >= MinGapSizeSmall);
97 @property uint MGS () const pure nothrow @safe @nogc { pragma(inline, true); return (mSingleLine ? MinGapSizeSmall : MinGapSize); }
99 protected:
100 char* tbuf; // text buffer
101 HighState* hbuf; // highlight buffer
102 uint tbused; // not including gap
103 uint tbsize; // including gap
104 uint tbmax = 512*1024*1024+MinGapSize; // maximum buffer size
105 uint gapstart, gapend; // tbuf[gapstart..gapend]; gap cannot be empty
106 uint bufferChangeCounter; // will simply increase on each buffer change
108 static private bool xrealloc(T) (ref T* ptr, ref uint cursize, int newsize, uint gran) nothrow @trusted @nogc {
109 import core.stdc.stdlib : realloc;
110 assert(gran > 1);
111 uint nsz = ((newsize+gran-1)/gran)*gran;
112 assert(nsz >= newsize);
113 T* nb = cast(T*)realloc(ptr, nsz*T.sizeof);
114 if (nb is null) return false;
115 cursize = nsz;
116 ptr = nb;
117 return true;
120 final:
121 // initial alloc
122 void initTBuf (bool hadHBuf) nothrow @nogc {
123 import core.stdc.stdlib : free, malloc, realloc;
124 assert(tbuf is null);
125 assert(hbuf is null);
126 immutable uint nsz = (mSingleLine ? GrowGranSmall : GrowGran);
127 tbuf = cast(char*)malloc(nsz);
128 if (tbuf is null) assert(0, "out of memory for text buffers");
129 // allocate highlight buffer if necessary
130 if (hadHBuf) {
131 hbuf = cast(HighState*)malloc(nsz*hbuf[0].sizeof);
132 if (hbuf is null) assert(0, "out of memory for text buffers");
133 hbuf[0..nsz] = HighState.init;
135 tbused = 0;
136 tbsize = nsz;
137 gapstart = 0;
138 gapend = tbsize;
141 // ensure that we can place a text of size `size` in buffer, and will still have at least MGS bytes free
142 // may change `tbsize`, but will not touch `tbused`
143 bool growTBuf (uint size) nothrow @nogc {
144 if (size > tbmax) return false; // too big
145 immutable uint mingapsize = MGS; // desired gap buffer size
146 immutable uint unused = tbsize-tbused; // number of unused bytes in buffer
147 assert(tbused <= tbsize);
148 if (size <= tbused && unused >= mingapsize) return true; // nothing to do, we have enough room in buffer
149 // if the gap is bigger than the minimal gap size, check if we have enough extra bytes to avoid allocation
150 if (unused > mingapsize) {
151 immutable uint extra = unused-mingapsize; // extra bytes we can spend
152 immutable uint bgrow = size-tbused; // number of bytes we need
153 if (extra >= bgrow) return true; // yay, no need to realloc
155 // have to grow
156 immutable uint newsz = size+mingapsize;
157 immutable uint gran = (mSingleLine ? GrowGranSmall : GrowGran);
158 uint hbufsz = tbsize;
159 if (!xrealloc(tbuf, tbsize, newsz, gran)) return false;
160 // reallocate highlighting buffer only if we already have one
161 if (hbuf !is null) {
162 if (!xrealloc(hbuf, hbufsz, newsz, gran)) { tbsize = hbufsz; return false; } // HACK!
163 assert(tbsize == hbufsz);
165 assert(tbsize >= newsz);
166 return true;
169 protected:
170 uint pos2real (uint pos) const pure @safe nothrow @nogc {
171 pragma(inline, true);
172 return pos+(pos >= gapstart ? gapend-gapstart : 0);
175 public:
176 HighState defhs; /// default highlighting state for new text
178 public:
180 this (bool asingleline) nothrow @nogc {
181 mSingleLine = asingleline;
182 initTBuf(false); // don't allocate hbuf yet
186 ~this () nothrow @nogc {
187 import core.stdc.stdlib : free;
188 if (tbuf !is null) free(tbuf);
189 if (hbuf !is null) free(hbuf);
192 /// remove all text from buffer
193 /// WILL NOT call deletion hooks!
194 void clear () nothrow @nogc {
195 import core.stdc.stdlib : free;
196 immutable bool hadHBuf = (hbuf !is null);
197 if (tbuf !is null) { free(tbuf); tbuf = null; }
198 if (hadHBuf) { free(hbuf); hbuf = null; }
199 ++bufferChangeCounter;
200 initTBuf(hadHBuf);
203 @property bool hasHiBuffer () const pure nothrow @safe @nogc { pragma(inline, true); return (hbuf !is null); } ///
205 /// after calling this with `true`, `hasHiBuffer` may still be false if there is no memory for it
206 @property void hasHiBuffer (bool v) nothrow @trusted @nogc {
207 if (v != hasHiBuffer) {
208 if (v) {
209 // create highlighting buffer
210 import core.stdc.stdlib : malloc;
211 assert(hbuf is null);
212 assert(tbsize > 0);
213 hbuf = cast(HighState*)malloc(tbsize*hbuf[0].sizeof);
214 if (hbuf !is null) hbuf[0..tbsize] = HighState.init;
215 } else {
216 // remove highlighitng buffer
217 import core.stdc.stdlib : free;
218 assert(hbuf !is null);
219 free(hbuf);
220 hbuf = null;
225 /// "single line" mode, for line editors
226 bool singleline () const pure @safe nothrow @nogc { pragma(inline, true); return mSingleLine; }
228 /// size of text buffer without gap, in one-byte chars
229 @property int textsize () const pure @safe nothrow @nogc { pragma(inline, true); return tbused; }
231 @property char opIndex (uint pos) const pure @trusted nothrow @nogc { pragma(inline, true); return (pos < tbused ? tbuf[pos+(pos >= gapstart ? gapend-gapstart : 0)] : '\n'); } ///
232 @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)); } ///
234 @property dchar uniAt (uint pos) const @trusted nothrow @nogc {
235 immutable ts = tbused;
236 if (pos >= ts) return '\n';
237 Utf8DecoderFast udc;
238 while (pos < ts) {
239 if (udc.decodeSafe(cast(ubyte)tbuf[pos2real(pos++)])) return udc.codepoint;
241 return udc.codepoint;
244 @property dchar uniAtAndAdvance (ref int pos) const @trusted nothrow @nogc {
245 immutable ts = tbused;
246 if (pos < 0) pos = 0;
247 if (pos >= ts) return '\n';
248 Utf8DecoderFast udc;
249 while (pos < ts) {
250 if (udc.decodeSafe(cast(ubyte)tbuf[pos2real(pos++)])) return udc.codepoint;
252 return udc.codepoint;
255 /// return utf-8 character length at buffer position pos or -1 on error (or 1 on error if "always positive")
256 /// never returns zero
257 int utfuckLenAt(bool alwaysPositive=true) (int pos) const @trusted nothrow @nogc {
258 immutable ts = tbused;
259 if (pos < 0 || pos >= ts) {
260 static if (alwaysPositive) return 1; else return -1;
262 char ch = tbuf[pos2real(pos)];
263 if (ch < 128) return 1;
264 Utf8DecoderFast udc;
265 auto spos = pos;
266 while (pos < ts) {
267 ch = tbuf[pos2real(pos++)];
268 if (udc.decode(cast(ubyte)ch)) {
269 static if (alwaysPositive) {
270 return (udc.invalid ? 1 : pos-spos);
271 } else {
272 return (udc.invalid ? -1 : pos-spos);
276 static if (alwaysPositive) return 1; else return -1;
279 // ensure that the buffer has room for at least one char in the gap
280 // note that this may move gap
281 protected void ensureGap () pure @safe nothrow @nogc {
282 pragma(inline, true);
283 // if we have zero-sized gap, assume that it is at end; we always have a room for at least MinGapSize(Small) chars
284 if (gapstart >= gapend || gapstart >= tbused) {
285 assert(tbused <= tbsize);
286 gapstart = tbused;
287 gapend = tbsize;
288 assert(gapend-gapstart >= MGS);
292 /// put the gap *before* `pos`
293 void moveGapAtPos (int pos) @trusted nothrow @nogc {
294 import core.stdc.string : memmove;
295 immutable ts = tbused; // i will need it in several places
296 if (pos < 0) pos = 0;
297 if (pos > ts) pos = ts;
298 if (ts == 0) { gapstart = 0; gapend = tbsize; return; } // unlikely case, but...
299 ensureGap(); // we should have a gap
300 /* cases:
301 * pos is before gap: shift [pos..gapstart] to gapend-len, shift gap
302 * pos is after gap: shift [gapend..pos] to gapstart, shift gap
304 if (pos < gapstart) {
305 // pos is before gap
306 int len = gapstart-pos; // to shift
307 memmove(tbuf+gapend-len, tbuf+pos, len);
308 if (hbuf !is null) memmove(hbuf+gapend-len, hbuf+pos, len*hbuf[0].sizeof);
309 gapstart -= len;
310 gapend -= len;
311 } else if (pos > gapstart) {
312 // pos is after gap
313 int len = pos-gapstart;
314 memmove(tbuf+gapstart, tbuf+gapend, len);
315 if (hbuf !is null) memmove(hbuf+gapstart, hbuf+gapend, len*hbuf[0].sizeof);
316 gapstart += len;
317 gapend += len;
319 // if we moved gap to buffer end, grow it; `ensureGap()` will do it for us
320 ensureGap();
321 assert(gapstart == pos);
322 assert(gapstart < gapend);
323 assert(gapstart <= ts);
326 /// put the gap at the end of the text
327 void moveGapAtEnd () @trusted nothrow @nogc { moveGapAtPos(tbused); }
329 /// put text into buffer; will either put all the text, or nothing
330 /// returns success flag
331 bool append (const(char)[] str...) @trusted nothrow @nogc { return (put(tbused, str) >= 0); }
333 /// put text into buffer; will either put all the text, or nothing
334 /// returns new position or -1
335 int put (int pos, const(char)[] str...) @trusted nothrow @nogc {
336 import core.stdc.string : memcpy;
337 if (pos < 0) pos = 0;
338 bool atend = (pos >= tbused);
339 if (atend) pos = tbused;
340 if (str.length == 0) return pos;
341 if (tbmax-(tbsize-tbused) < str.length) return -1; // no room
342 if (!growTBuf(tbused+cast(uint)str.length)) return -1; // memory allocation failed
343 //TODO: this can be made faster, but meh...
344 immutable slen = cast(uint)str.length;
345 if (atend || gapend-gapstart < slen) moveGapAtEnd(); // this will grow the gap, so it will take all available room
346 if (!atend) moveGapAtPos(pos); // condition is used for tiny speedup
347 assert(gapend-gapstart >= slen);
348 memcpy(tbuf+gapstart, str.ptr, str.length);
349 if (hbuf !is null) hbuf[gapstart..gapstart+str.length] = defhs;
350 gapstart += slen;
351 tbused += slen;
352 pos += slen;
353 ensureGap();
354 assert(tbsize-tbused >= MGS);
355 ++bufferChangeCounter;
356 return pos;
359 /// remove count bytes from the current position; will either remove all of 'em, or nothing
360 /// returns success flag
361 bool remove (int pos, int count) @trusted nothrow @nogc {
362 import core.stdc.string : memmove;
363 if (count < 0) return false;
364 if (count == 0) return true;
365 immutable ts = tbused; // cache current text size
366 if (pos < 0) pos = 0;
367 if (pos > ts) pos = ts;
368 if (ts-pos < count) return false; // not enough text here
369 assert(gapstart < gapend);
370 ++bufferChangeCounter; // buffer will definitely be changed
371 for (;;) {
372 // at the start of the gap: i can just increase gap
373 if (pos == gapstart) {
374 gapend += count;
375 tbused -= count;
376 return true;
378 // removing text just before gap: increase gap (backspace does this)
379 if (pos+count == gapstart) {
380 gapstart -= count;
381 tbused -= count;
382 assert(gapstart == pos);
383 return true;
385 // both variants failed; move gap at `pos` and try again
386 moveGapAtPos(pos);
390 /// count how much eols we have in this range
391 int countEolsInRange (int pos, int count) const @trusted nothrow @nogc {
392 import core.stdc.string : memchr;
393 if (count < 1 || pos <= -count || pos >= tbused) return 0;
394 if (pos+count > tbused) count = tbused-pos;
395 int res = 0;
396 while (count > 0) {
397 int npos = fastFindCharIn(pos, count, '\n')+1;
398 if (npos <= 0) break;
399 ++res;
400 count -= (npos-pos);
401 pos = npos;
403 return res;
406 /// using `memchr`, jumps over gap; never moves after `tbused`
407 public uint fastSkipEol (int pos) const @trusted nothrow @nogc {
408 import core.stdc.string : memchr;
409 immutable ts = tbused;
410 if (ts == 0 || pos >= ts) return ts;
411 if (pos < 0) pos = 0;
412 // check text before gap
413 if (pos < gapstart) {
414 auto fp = cast(char*)memchr(tbuf+pos, '\n', gapstart-pos);
415 if (fp !is null) return cast(int)(fp-tbuf)+1;
416 pos = gapstart; // new starting position
418 assert(pos >= gapstart);
419 // check after gap and to text end
420 int left = ts-pos;
421 if (left > 0) {
422 auto stx = tbuf+gapend+(pos-gapstart);
423 assert(cast(usize)(tbuf+tbsize-stx) >= left);
424 auto fp = cast(char*)memchr(stx, '\n', left);
425 if (fp !is null) return pos+cast(int)(fp-stx)+1;
427 return ts;
430 /// using `memchr`, jumps over gap; returns `tbused` if not found
431 public uint fastFindChar (int pos, char ch) const @trusted nothrow @nogc {
432 int res = fastFindCharIn(pos, tbused, ch);
433 return (res >= 0 ? res : tbused);
436 /// use `memchr`, jumps over gap; returns -1 if not found
437 public int fastFindCharIn (int pos, int len, char ch) const @trusted nothrow @nogc {
438 import core.stdc.string : memchr;
439 immutable ts = tbused;
440 if (len < 1) return -1;
441 if (ts == 0 || pos >= ts) return -1;
442 if (pos < 0) {
443 if (pos <= -len) return -1;
444 len += pos;
445 pos = 0;
447 if (tbused-pos < len) len = tbused-pos;
448 assert(len > 0);
449 int left;
450 // check text before gap
451 if (pos < gapstart) {
452 left = gapstart-pos;
453 if (left > len) left = len;
454 auto fp = cast(char*)memchr(tbuf+pos, ch, left);
455 if (fp !is null) return cast(int)(fp-tbuf);
456 if ((len -= left) == 0) return -1;
457 pos = gapstart; // new starting position
459 assert(pos >= gapstart);
460 // check after gap and to text end
461 left = ts-pos;
462 if (left > len) left = len;
463 if (left > 0) {
464 auto stx = tbuf+gapend+(pos-gapstart);
465 assert(cast(usize)(tbuf+tbsize-stx) >= left);
466 auto fp = cast(char*)memchr(stx, ch, left);
467 if (fp !is null) return pos+cast(int)(fp-stx);
469 return -1;
472 /// bufparts range
473 /// this is hack for regexp searchers
474 /// do not store returned slice anywhere for a long time!
475 /// slice *will* be invalidated on next gap buffer operation!
476 public auto bufparts (int pos) nothrow @nogc {
477 static struct Range {
478 nothrow @nogc:
479 GapBuffer gb;
480 bool aftergap; // fr is "aftergap"?
481 const(char)[] fr;
482 private this (GapBuffer agb, int pos) {
483 gb = agb;
484 auto ts = agb.tbused;
485 if (ts == 0 || pos >= ts) { gb = null; return; }
486 if (pos < 0) pos = 0;
487 if (pos < agb.gapstart) {
488 fr = agb.tbuf[pos..agb.gapstart];
489 } else {
490 int left = ts-pos;
491 if (left < 1) { gb = null; return; }
492 pos -= agb.gapstart;
493 fr = agb.tbuf[agb.gapend+pos..agb.gapend+pos+left];
494 aftergap = true;
497 @property bool empty () pure const @safe { pragma(inline, true); return (gb is null); }
498 @property const(char)[] front () pure @safe { pragma(inline, true); return fr; }
499 void popFront () {
500 if (aftergap) gb = null;
501 if (gb is null) { fr = null; return; }
502 int left = gb.textsize-gb.gapstart;
503 if (left < 1) { gb = null; fr = null; return; }
504 fr = gb.tbuf[gb.gapend..gb.gapend+left];
505 aftergap = true;
508 return Range(this, pos);
511 /// this calls dg with continuous buffer parts, so you can write 'em to a file, for example
512 public final void forEachBufPart (int pos, int len, scope void delegate (const(char)[] buf) dg) {
513 if (dg is null) return;
514 immutable ts = tbused;
515 if (len < 1) return;
516 if (ts == 0 || pos >= ts) return;
517 if (pos < 0) {
518 if (pos <= -len) return;
519 len += pos;
520 pos = 0;
522 assert(len > 0);
523 int left;
524 // check text before gap
525 if (pos < gapstart) {
526 left = gapstart-pos;
527 if (left > len) left = len;
528 assert(left > 0);
529 dg(tbuf[pos..pos+left]);
530 if ((len -= left) == 0) return; // nothing more to do
531 pos = gapstart; // new starting position
533 assert(pos >= gapstart);
534 // check after gap and to text end
535 left = ts-pos;
536 if (left > len) left = len;
537 if (left > 0) {
538 auto stx = tbuf+gapend+(pos-gapstart);
539 assert(cast(usize)(tbuf+tbsize-stx) >= left);
540 dg(stx[0..left]);
544 public:
546 static bool hasEols (const(char)[] str) pure nothrow @trusted @nogc {
547 import core.stdc.string : memchr;
548 uint left = cast(uint)str.length;
549 return (left > 0 && memchr(str.ptr, '\n', left) !is null);
552 /// index or -1
553 static int findEol (const(char)[] str) pure nothrow @trusted @nogc {
554 if (str.length > int.max) assert(0, "string too long");
555 int left = cast(int)str.length;
556 if (left > 0) {
557 import core.stdc.string : memchr;
558 auto dp = cast(const(char)*)memchr(str.ptr, '\n', left);
559 if (dp !is null) return cast(int)(dp-str.ptr);
561 return -1;
564 /// count number of '\n' chars in string
565 static int countEols (const(char)[] str) pure nothrow @trusted @nogc {
566 import core.stdc.string : memchr;
567 if (str.length > int.max) assert(0, "string too long");
568 int count = 0;
569 uint left = cast(uint)str.length;
570 auto dsp = str.ptr;
571 while (left > 0) {
572 auto ep = cast(const(char)*)memchr(dsp, '\n', left);
573 if (ep is null) break;
574 ++count;
575 ++ep;
576 left -= cast(uint)(ep-dsp);
577 dsp = ep;
579 return count;
584 // ////////////////////////////////////////////////////////////////////////// //
585 // Self-Healing Line Cache (utm) implementation
586 //TODO(?): don't do full cache repairing
587 private final class LineCache {
588 private:
589 // line offset/height cache item
590 // to be safe, locache[mLineCount] is always valid and holds gb.textsize
591 static align(1) struct LOCItem {
592 align(1):
593 uint ofs;
594 private uint mheight; // 0: unknown; line height; high bit is reserved for "viswrap" flag
595 pure nothrow @safe @nogc:
596 @property bool validHeight () const { pragma(inline, true); return ((mheight&0x7fff_ffffU) != 0); }
597 @property void resetHeight () { pragma(inline, true); mheight &= 0x8000_0000U; } // doesn't reset "viswrap" flag
598 @property uint height () const { pragma(inline, true); return (mheight&0x7fff_ffffU); }
599 @property void height (uint v) { pragma(inline, true); assert(v <= 0x7fff_ffffU); mheight = (mheight&0x8000_0000)|v; }
600 @property bool viswrap () const { pragma(inline, true); return ((mheight&0x8000_0000U) != 0); }
601 @property viswrap (bool v) { pragma(inline, true); if (v) mheight |= 0x8000_0000U; else mheight &= 0x7fff_ffffU; }
602 @property void resetHeightAndWrap () { pragma(inline, true); mheight = 0; }
603 @property void resetHeightAndSetWrap () { pragma(inline, true); mheight = 0x8000_0000U; }
604 void initWithPos (uint pos) { pragma(inline, true); ofs = pos; mheight = 0; }
607 private:
608 GapBuffer gb;
609 LOCItem* locache; // line info cache
610 uint locsize; // number of allocated items in locache
611 uint mLineCount; // total number of lines (and valid items in cache too)
612 EgTextMeter textMeter; // null: monospaced font
613 int mLineHeight = 1; // line height, in pixels/cells; <0: variable; 0: invalid state
614 dchar delegate (char ch) nothrow recode1byte; // not null: delegate to recode from 1bt to unishit
615 int mWordWrapWidth = 0; // >0: "visual wrap"; either in pixels (if textMeter is set) or in cells
617 public:
618 // utfuck-8 and visual tabs support
619 // WARNING! this will SIGNIFICANTLY slow down coordinate calculations!
620 bool utfuck = false; /// should x coordinate calculation assume that the text is in UTF-8?
621 bool visualtabs = false; /// should x coordinate calculation assume that tabs are not one-char width?
622 ubyte tabsize = 2; /// tab size, in spaces
624 final: // just in case the compiler won't notice "final class"
625 private:
626 void initLC () nothrow @nogc {
627 import core.stdc.stdlib : realloc;
628 // allocate initial line cache
629 uint ICS = (gb.mSingleLine ? 2 : 1024);
630 locache = cast(typeof(locache[0])*)realloc(locache, ICS*locache[0].sizeof);
631 if (locache is null) assert(0, "out of memory for line cache");
632 locache[0..ICS] = LOCItem.init;
633 locache[1].ofs = gb.textsize; // just in case
634 locsize = ICS;
635 mLineCount = 1; // we always have at least one line, even if it is empty
638 // `total` is new number of entries in cache; actual number will be greater by one
639 bool growLineCache (uint total) nothrow @nogc {
640 if (total >= int.max/8) assert(0, "egeditor: wtf?!");
641 ++total; // for last item
642 if (locsize < total) {
643 // have to allocate more
644 if (!GapBuffer.xrealloc(locache, locsize, total, (locsize < 4096 ? 0x400 : 0x1000))) return false;
646 return true;
649 int calcLineHeight (int lidx) nothrow {
650 assert(lidx >= 0 && lidx < mLineCount);
651 int ls = locache[lidx].ofs;
652 int le = locache[lidx+1].ofs;
653 if (locache[lidx].viswrap) ++le;
654 textMeter.reset(-1); // nobody cares about tab widths here
655 scope(exit) textMeter.finish();
656 int maxh = textMeter.currheight;
657 if (maxh < 1) maxh = 1;
658 auto tbufcopy = gb.tbuf;
659 auto hbufcopy = gb.hbuf;
660 gb.hidummy = GapBuffer.HighState.init; // just in case
661 if (utfuck) {
662 Utf8DecoderFast udc;
663 GapBuffer.HighState* hs = (hbufcopy !is null ? hbufcopy+gb.pos2real(ls) : &gb.hidummy);
664 while (ls < le) {
665 char ch = tbufcopy[gb.pos2real(ls++)];
666 if (udc.decodeSafe(cast(ubyte)ch)) {
667 immutable dchar dch = udc.codepoint;
668 textMeter.advance(dch, *hs);
670 if (textMeter.currheight > maxh) maxh = textMeter.currheight;
671 if (ls < le && hbufcopy !is null) hs = hbufcopy+gb.pos2real(ls);
673 } else {
674 auto rc1b = recode1byte;
675 while (ls < le) {
676 immutable uint rpos = gb.pos2real(ls++);
677 dchar dch = (rc1b !is null ? rc1b(tbufcopy[rpos]) : cast(dchar)tbufcopy[rpos]);
678 GapBuffer.HighState* hs = (hbufcopy !is null ? hbufcopy+rpos : &gb.hidummy);
679 textMeter.advance(dch, *hs);
680 if (textMeter.currheight > maxh) maxh = textMeter.currheight;
683 //{ import core.stdc.stdio; printf("line #%d height is %d\n", lidx, maxh); }
684 return maxh;
687 // -1: not found
688 int findLineCacheIndex (uint pos) const nothrow @nogc {
689 int lcount = mLineCount;
690 if (lcount <= 0) return -1;
691 if (pos >= gb.tbused) return lcount-1;
692 if (lcount == 1) return (pos < locache[1].ofs ? 0 : -1);
693 if (pos < locache[lcount].ofs) {
694 // yay! use binary search to find the line
695 int bot = 0, i = lcount-1;
696 while (bot != i) {
697 int mid = i-(i-bot)/2;
698 //!assert(mid >= 0 && mid < locused);
699 immutable ls = locache[mid].ofs;
700 immutable le = locache[mid+1].ofs;
701 if (pos >= ls && pos < le) return mid; // i found her!
702 if (pos < ls) i = mid-1; else bot = mid;
704 return i;
706 return -1;
709 // get range for current wrapped line: [ltop..lbot)
710 void wwGetTopBot (int lidx, int* ltop, int* lbot) const nothrow @trusted @nogc {
711 if (lidx < 0) lidx = 0;
712 if (lidx >= mLineCount) { *ltop = *lbot = mLineCount; return; }
713 *ltop = *lbot = lidx;
714 if (mWordWrapWidth <= 0) { *lbot += 1; return; }
715 immutable bool curIsMiddle = locache[lidx].viswrap;
716 // find first (top) line move up to previous unwrapped, and then one down
717 if (lidx > 0) while (lidx > 0 && locache[lidx-1].viswrap) --lidx;
718 *ltop = lidx;
719 // find last (bottom) line (if current is wrapped, move down to first unwrapped; then one down anyway)
720 lidx = *lbot;
721 if (curIsMiddle) while (lidx < mLineCount && locache[lidx].viswrap) ++lidx;
722 if (++lidx > mLineCount) lidx = mLineCount; // just in case
723 *lbot = lidx;
724 assert(*ltop < *lbot);
727 int collapseWrappedLine (int lidx) nothrow @nogc {
728 import core.stdc.string : memmove;
729 if (mWordWrapWidth <= 0 || gb.mSingleLine || lidx < 0 || lidx >= mLineCount) return lidx;
730 if (!locache[lidx].viswrap) return lidx; // early exit
731 int ltop, lbot;
732 wwGetTopBot(lidx, &ltop, &lbot);
733 immutable int tokill = lbot-ltop-1;
734 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); }
735 if (tokill <= 0) return lidx; // nothing to do
736 // remove cache items for wrapped lines
737 if (lbot <= mLineCount) memmove(locache+ltop+1, locache+lbot, (mLineCount-lbot+1)*locache[0].sizeof);
738 lbot -= tokill;
739 mLineCount -= tokill;
740 // and fix wrapping flag
741 locache[ltop].resetHeightAndWrap();
742 return ltop;
745 // do word wrapping; line cache should be calculated and repaired for the given line
746 // (and down, if it was already wrapped)
747 // returns next line index to possible wrap
748 //TODO: "unwrap" line if word wrapping is not set?
749 //TODO: make it faster
750 //TODO: unicode blanks?
751 int doWordWrapping (int lidx) nothrow {
752 import core.stdc.string : memmove;
754 static bool isBlank (char ch) pure nothrow @safe @nogc { pragma(inline, true); return (ch == '\t' || ch == ' '); }
756 immutable int www = mWordWrapWidth;
757 if (www <= 0 || gb.mSingleLine) return mLineCount;
758 if (lidx < 0) lidx = 0;
759 if (lidx >= mLineCount) return mLineCount;
760 // find first and last line, if it was already wrapped
761 int ltop, lbot;
762 wwGetTopBot(lidx, &ltop, &lbot);
763 // now the hard part
764 if (textMeter) textMeter.reset(visualtabs ? tabsize : 0);
765 scope(exit) textMeter.finish();
766 version(none) { import core.stdc.stdio; printf("lidx=%d; ltop=%d; lbot=%d, linecount=%d\n", lidx, ltop, lbot, mLineCount); }
767 // we have line range here; go, ninja, go
768 lidx = ltop;
769 int cpos = locache[lidx].ofs;
770 int cwdt = 0;
771 int lastWordStartPos = 0;
772 immutable bool utfuckmode = utfuck;
773 immutable int tabsz = (visualtabs ? tabsize : 0);
774 auto tm = textMeter;
775 while (gb[cpos] != '\n') {
776 // go until cwdt allows us; but we have to have at least one char
777 int nwdt; // "new" width with the current char
778 int lpos = cpos; // "last position"
779 if (tm is null) {
780 if (tabsz > 0 && gb[cpos] == '\t') {
781 // skip tab
782 nwdt = ((cwdt+tabsz)/tabsz)*tabsz;
783 } else {
784 nwdt = cwdt+1;
785 if (utfuckmode) cpos += gb.utfuckLenAt!true(cpos); else ++cpos;
787 } else {
788 dchar dch = (utfuckmode ? gb.uniAtAndAdvance(cpos) : recode1byte is null ? cast(dchar)gb[cpos++] : recode1byte(gb[cpos++]));
789 tm.advance(dch, gb.hi(lpos));
790 nwdt = tm.currwdt;
792 //{ 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); }
793 // if we have at least one char in line, check if we should wrap here
794 if (lpos > locache[lidx].ofs) {
795 if (nwdt > www) {
796 // should wrap here
797 if (tm !is null) {
798 tm.finish();
799 tm.reset(visualtabs ? tabsize : 0);
801 // if we have at least one word, wrap on it
802 if (lastWordStartPos > 0) {
803 cpos = lastWordStartPos;
804 lastWordStartPos = 0;
805 } else {
806 cpos = lpos;
808 locache[lidx].resetHeightAndSetWrap();
809 ++lidx;
810 if (lidx == lbot) {
811 // insert new cache record
812 growLineCache(mLineCount+1);
813 if (lidx <= mLineCount) memmove(locache+lidx+1, locache+lidx, (mLineCount-lidx+1)*locache[0].sizeof);
814 ++mLineCount;
815 ++lbot;
817 // setup next line
818 locache[lidx] = LOCItem.init; // reset all, including height and wrapping
819 locache[lidx].ofs = cpos;
820 cwdt = 0;
821 continue;
822 } else {
823 // check for word boundary
824 if (isBlank(gb[lpos]) && !isBlank(gb[cpos])) lastWordStartPos = cpos;
827 // go on
828 cwdt = nwdt;
830 locache[lidx].resetHeightAndWrap();
831 // remove unused cache items
832 version(none) { import core.stdc.stdio; printf(" 00: lidx=%d; ltop=%d; lbot=%d, linecount=%d\n", lidx, ltop, lbot, mLineCount); }
833 assert(lidx < lbot);
834 //TODO: make it faster
835 ++lidx; // to kill: [lidx..lbot)
836 immutable int tokill = lbot-lidx;
837 if (tokill > 0) {
838 version(none) { import core.stdc.stdio; printf(" xx: lidx=%d; ltop=%d; lbot=%d, linecount=%d; tokill=%d\n", lidx, ltop, lbot, mLineCount, tokill); }
839 if (lbot <= mLineCount) memmove(locache+lidx, locache+lbot, (mLineCount-lbot+1)*locache[0].sizeof);
840 lbot -= tokill;
841 mLineCount -= tokill;
843 version(none) { import core.stdc.stdio; printf(" 01: lidx=%d; ltop=%d; lbot=%d, linecount=%d\n", lidx, ltop, lbot, mLineCount); }
844 assert(lidx == lbot);
845 assert(mLineCount > 0);
846 assert(locache[lbot].ofs == cpos+(cpos < gb.textsize ? 1 : 0));
847 return lbot;
850 public:
851 this (GapBuffer agb) nothrow @nogc {
852 assert(agb !is null);
853 gb = agb;
854 initLC();
857 ~this () nothrow @nogc {
858 if (locache !is null) {
859 import core.stdc.stdlib : free;
860 free(locache);
864 public:
865 /// there is always at least one line, so `linecount` is never zero
866 @property int linecount () const pure nothrow @safe @nogc { pragma(inline, true); return mLineCount; }
868 void clear () nothrow @nogc {
869 import core.stdc.stdlib : free;
870 gb.clear();
871 // free old buffers
872 if (locache !is null) { free(locache); locache = null; }
873 // allocate new buffer
874 initLC();
877 /** load file like this:
878 * if (!lc.resizeBuffer(filesize)) throw new Exception("memory?");
879 * scope(failure) lc.clear();
880 * fl.rawReadExact(lc.getBufferPtr[]);
881 * if (!lc.rebuild()) throw new Exception("memory?");
884 /// allocate text buffer for the text of the given size
885 bool resizeBuffer (uint newsize) nothrow @nogc {
886 if (newsize > gb.tbmax) return false;
887 clear();
888 //{ import core.stdc.stdio; printf("resizing buffer to %u bytes\n", newsize); }
889 if (!gb.growTBuf(newsize)) return false;
890 gb.tbused = gb.gapstart = newsize;
891 gb.gapend = gb.tbsize;
892 gb.ensureGap();
893 return true;
896 /// get continuous buffer pointer, so we can read the whole file into it
897 char[] getBufferPtr () nothrow @nogc {
898 gb.moveGapAtEnd();
899 return gb.tbuf[0..gb.textsize];
902 /// count lines, fill line cache, do word wrapping
903 bool rebuild () nothrow {
904 import core.stdc.string : memset;
905 //gb.moveGapAtEnd(); // just in case
906 immutable ts = gb.textsize;
907 const(char)* tb = gb.tbuf;
908 if (gb.mSingleLine) {
909 // easy
910 growLineCache(1);
911 assert(locsize > 0);
912 mLineCount = 1;
913 locache[0..2] = LOCItem.init;
914 locache[1].ofs = ts;
915 return true;
917 version(egeditor_scan_time) auto stt = clockMilli();
918 version(none) {
919 // less memory fragmentation
920 int lcount = gb.countEolsInRange(0, ts)+1; // total number of lines
921 //{ import core.stdc.stdio; printf("loaded %u bytes; %d lines found\n", gb.textsize, lcount); }
922 if (!growLineCache(lcount)) return false;
923 assert(lcount+1 <= locsize);
924 //locache[0..lcount+1] = LOCItem.init; // reset all lines
925 memset(locache, 0, (lcount+1)*locache[0].sizeof); // reset all lines (help compiler a little)
926 uint pos = 0;
927 LOCItem* lcp = locache; // help compiler a little
928 foreach (immutable uint lidx; 0..lcount) {
929 lcp.initWithPos(pos);
930 ++lcp;
931 pos = gb.fastSkipEol(pos);
933 // last line
934 assert(lcp is locache+lcount);
935 lcp.ofs = gb.textsize;
936 } else {
937 // faster scanning
938 if (gb.textsize == 0) {
939 // no text
940 if (!growLineCache(1)) return false; // should have at least one
941 assert(locsize >= 2);
942 locache[0].initWithPos(0);
943 locache[1].initWithPos(0);
944 mLineCount = 1;
945 } else {
946 int lcount = 0; // total number of lines
947 //{ import core.stdc.stdio; printf("loaded %u bytes; %d lines found\n", gb.textsize, lcount); }
948 if (!growLineCache(1)) return false; // should have at least one
949 uint pos = 0;
950 while (pos < ts) {
951 if (lcount+1 >= locsize) { if (!growLineCache(lcount+1)) return false; }
952 locache[lcount++].initWithPos(pos);
953 pos = gb.fastSkipEol(pos);
955 assert(lcount > 0);
956 // hack for last empty line, if it ends with '\n'
957 if (ts && gb[ts-1] == '\n') {
958 if (lcount+1 >= locsize) { if (!growLineCache(lcount+1)) return false; }
959 locache[lcount++].initWithPos(ts);
961 // last line
962 assert(pos == ts);
963 assert(lcount < locsize);
964 locache[lcount].initWithPos(pos);
965 mLineCount = lcount;
968 version(egeditor_scan_time) {
969 import core.stdc.stdio;
970 auto et = clockMilli()-stt;
971 version(egeditor_scan_time_to_file) {
972 if (auto fo = fopen("ztime.log", "a")) {
973 scope(exit) fo.fclose();
974 fo.fprintf("%u lines (%u bytes) scanned in %u milliseconds\n", mLineCount, gb.textsize, cast(uint)et);
976 } else {
977 printf("%u lines (%u bytes) scanned in %u milliseconds\n", mLineCount, gb.textsize, cast(uint)et);
979 stt = clockMilli(); // for wrapping
981 if (mWordWrapWidth > 0) {
982 int lidx = 0;
983 while (lidx < mLineCount) lidx = doWordWrapping(lidx);
984 version(egeditor_scan_time) {
985 import core.stdc.stdio;
986 et = clockMilli()-stt;
987 version(egeditor_scan_time_to_file) {
988 if (auto fo = fopen("ztime.log", "a")) {
989 scope(exit) fo.fclose();
990 fo.fprintf(" %u lines wrapped in %u milliseconds\n", mLineCount, cast(uint)et);
992 } else {
993 printf(" %u lines wrapped in %u milliseconds\n", mLineCount, cast(uint)et);
997 return true;
1000 /// put text into buffer; will either put all the text, or nothing
1001 /// returns success flag
1002 bool append (const(char)[] str...) nothrow { return (put(gb.textsize, str) >= 0); }
1004 /// put text into buffer; will either put all the text, or nothing
1005 /// returns new position or -1
1006 int put (int pos, const(char)[] str...) nothrow {
1007 if (pos < 0) pos = 0;
1008 bool atend = (pos >= gb.textsize);
1009 if (str.length == 0) return pos;
1010 if (atend) pos = gb.textsize;
1011 auto ppos = gb.put(pos, str);
1012 if (ppos < 0) return ppos;
1013 // heal line cache for single-line case
1014 if (gb.mSingleLine) {
1015 assert(mLineCount == 1);
1016 assert(locsize > 1);
1017 assert(locache[0].ofs == 0);
1018 locache[1].ofs = gb.textsize;
1019 locache[0].resetHeightAndWrap();
1020 locache[1].resetHeightAndWrap();
1021 } else {
1022 assert(ppos > pos);
1023 int newlines = GapBuffer.countEols(str);
1024 immutable insertedLines = newlines;
1025 auto lidx = findLineCacheIndex(pos);
1026 immutable int ldelta = ppos-pos;
1027 assert((!atend && lidx >= 0) || (atend && (lidx < 0 || lidx == mLineCount-1)));
1028 if (atend) lidx = mLineCount-1;
1029 int wraplidx = lidx;
1030 if (newlines == 0) {
1031 // no lines was inserted, just repair the length
1032 // no need to collapse wrapped line here, 'cause `doWordWrapping()` will rewrap it anyway
1033 locache[lidx++].resetHeight();
1034 } else {
1035 import core.stdc.string : memmove;
1036 //FIXME: make this faster for wrapped lines
1037 wraplidx = lidx = collapseWrappedLine(wraplidx);
1038 // we will start repairing from the last good line
1039 pos = locache[lidx].ofs;
1040 // inserted some new lines, make room for 'em
1041 growLineCache(mLineCount+newlines);
1042 if (lidx <= mLineCount) memmove(locache+lidx+newlines, locache+lidx, (mLineCount-lidx+1)*locache[0].sizeof);
1043 mLineCount += newlines;
1044 // no need to clear inserted lines, we'll overwrite em
1045 // recalc offsets and lengthes
1046 while (newlines-- >= 0) {
1047 locache[lidx].ofs = pos;
1048 locache[lidx++].resetHeightAndWrap();
1049 pos = gb.fastSkipEol(pos);
1052 // repair line cache (offsets) -- for now; switch to "repair on demand" later?
1053 if (lidx <= mLineCount) foreach (ref lc; locache[lidx..mLineCount+1]) lc.ofs += ldelta;
1054 if (mWordWrapWidth > 0) {
1055 foreach (immutable c; 0..insertedLines+1) wraplidx = doWordWrapping(wraplidx);
1058 return ppos;
1061 /// remove count bytes from the current position; will either remove all of 'em, or nothing
1062 /// returns success flag
1063 bool remove (int pos, int count) nothrow {
1064 if (gb.mSingleLine) {
1065 // easy
1066 if (!gb.remove(pos, count)) return false;
1067 assert(mLineCount == 1);
1068 assert(locsize > 1);
1069 assert(locache[0].ofs == 0);
1070 locache[1].ofs = gb.textsize;
1071 locache[0].resetHeightAndWrap();
1072 locache[1].resetHeightAndWrap();
1073 } else {
1074 // hard
1075 import core.stdc.string : memmove;
1076 if (count < 0) return false;
1077 if (count == 0) return true;
1078 if (pos < 0) pos = 0;
1079 if (pos > gb.textsize) pos = gb.textsize;
1080 if (gb.textsize-pos < count) return false; // not enough text here
1081 auto lidx = findLineCacheIndex(pos);
1082 assert(lidx >= 0);
1083 int newlines = gb.countEolsInRange(pos, count);
1084 if (!gb.remove(pos, count)) return false;
1085 // repair line cache
1086 if (newlines == 0) {
1087 // no need to collapse wrapped line here, 'cause `doWordWrapping()` will rewrap it anyway
1088 locache[lidx].resetHeight();
1089 } else {
1090 import core.stdc.string : memmove;
1091 if (mWordWrapWidth > 0) {
1092 // collapse wordwrapped line into one, it is easier this way; it is safe to collapse lines in modified text
1093 //FIXME: make this faster for wrapped lines
1094 lidx = collapseWrappedLine(lidx);
1095 // collapse deleted lines too, so we can remove 'em as if they were normal ones
1096 foreach (immutable c; 1..newlines+1) collapseWrappedLine(lidx+c);
1098 // remove unused lines
1099 if (lidx+1 <= mLineCount) memmove(locache+lidx+1, locache+lidx+1+newlines, (mLineCount-lidx)*locache[0].sizeof);
1100 mLineCount -= newlines;
1101 // fix current line
1102 locache[lidx].resetHeightAndWrap();
1104 if (lidx+1 <= mLineCount) foreach (ref lc; locache[lidx+1..mLineCount+1]) lc.ofs -= count;
1105 if (mWordWrapWidth > 0) {
1106 lidx = doWordWrapping(lidx);
1107 // and next one, 'cause it was modified by collapser
1108 doWordWrapping(lidx);
1111 return true;
1114 int lineHeightPixels (int lidx, bool forceRecalc=false) nothrow {
1115 int h;
1116 assert(textMeter !is null);
1117 if (lidx < 0 || mLineCount == 0 || lidx >= mLineCount) {
1118 textMeter.reset(0);
1119 h = (textMeter.currheight > 0 ? textMeter.currheight : 1);
1120 textMeter.finish();
1121 } else {
1122 if (forceRecalc || !locache[lidx].validHeight) locache[lidx].height = calcLineHeight(lidx);
1123 h = locache[lidx].height;
1125 return h;
1128 bool isLastWrappedLine (int lidx) nothrow @nogc { pragma(inline, true); return (lidx >= 0 && lidx < mLineCount ? !locache[lidx].viswrap : true); }
1129 bool isWrappedLine (int lidx) nothrow @nogc { pragma(inline, true); return (lidx >= 0 && lidx < mLineCount ? locache[lidx].viswrap : false); }
1131 /// get number of *symbols* to line end (this is not always equal to number of bytes for utfuck)
1132 int syms2eol (int pos) nothrow {
1133 immutable ts = gb.textsize;
1134 if (pos < 0) pos = 0;
1135 if (pos >= ts) return 0;
1136 int epos = line2pos(pos2line(pos)+1);
1137 if (!utfuck) return epos-pos; // fast path
1138 // slow path
1139 int count = 0;
1140 while (pos < epos) {
1141 pos += gb.utfuckLenAt!true(pos);
1142 ++count;
1144 return count;
1147 /// get line for the given position
1148 int pos2line (int pos) nothrow {
1149 immutable ts = gb.textsize;
1150 if (pos < 0) return 0;
1151 if (pos == 0 || ts == 0) return 0;
1152 if (pos >= ts) return mLineCount-1; // end of text: no need to update line offset cache
1153 if (mLineCount == 1) return 0;
1154 int lcidx = findLineCacheIndex(pos);
1155 assert(lcidx >= 0 && lcidx < mLineCount);
1156 return lcidx;
1159 /// get position (starting) for the given line
1160 /// it will be 0 for negative lines, and `textsize` for positive out of bounds lines
1161 int line2pos (int lidx) nothrow {
1162 if (lidx < 0 || gb.textsize == 0) return 0;
1163 if (lidx > mLineCount-1) return gb.textsize;
1164 if (mLineCount == 1) {
1165 assert(lidx == 0);
1166 return 0;
1168 return locache[lidx].ofs;
1171 alias linestart = line2pos; /// ditto
1173 /// get ending position for the given line (position of '\n')
1174 /// it may be `textsize`, though, if this is the last line, and it doesn't end with '\n'
1175 int lineend (int lidx) nothrow {
1176 if (lidx < 0 || gb.textsize == 0) return 0;
1177 if (lidx > mLineCount-1) return gb.textsize;
1178 if (mLineCount == 1) {
1179 assert(lidx == 0);
1180 return gb.textsize;
1182 if (lidx == mLineCount-1) return gb.textsize;
1183 auto res = locache[lidx+1].ofs;
1184 assert(res > 0);
1185 return res-1;
1188 // move by `x` utfucked chars
1189 // `pos` should point to line start
1190 // will never go beyond EOL
1191 private int utfuck_x2pos (int x, int pos) nothrow {
1192 immutable ts = gb.textsize;
1193 const(char)* tbuf = gb.tbuf;
1194 if (pos < 0) pos = 0;
1195 if (mWordWrapWidth <= 0 || gb.mSingleLine) {
1196 if (gb.mSingleLine) {
1197 // single line
1198 while (pos < ts && x > 0) {
1199 pos += gb.utfuckLenAt!true(pos); // "always positive"
1200 --x;
1202 } else {
1203 // multiline
1204 while (pos < ts && x > 0) {
1205 if (tbuf[gb.pos2real(pos)] == '\n') break;
1206 pos += gb.utfuckLenAt!true(pos); // "always positive"
1207 --x;
1210 } else {
1211 if (pos >= ts) return ts;
1212 int lidx = findLineCacheIndex(pos);
1213 assert(lidx >= 0);
1214 int epos = locache[lidx+1].ofs;
1215 while (pos < epos && x > 0) {
1216 if (tbuf[gb.pos2real(pos)] == '\n') break;
1217 pos += gb.utfuckLenAt!true(pos); // "always positive"
1218 --x;
1221 if (pos > ts) pos = ts;
1222 return pos;
1225 // convert line offset to screen x coordinate
1226 // `pos` should point into line (somewhere)
1227 private int utfuck_pos2x(bool dotabs=false) (int pos) nothrow {
1228 immutable ts = gb.textsize;
1229 if (pos < 0) pos = 0;
1230 if (pos > ts) pos = ts;
1231 immutable bool sl = gb.mSingleLine;
1232 const(char)* tbuf = gb.tbuf;
1233 int x = 0;
1234 if (mWordWrapWidth <= 0) {
1235 // find line start
1236 int spos = pos;
1237 if (!sl) {
1238 while (spos > 0 && tbuf[gb.pos2real(spos-1)] != '\n') --spos;
1239 } else {
1240 spos = 0;
1242 // now `spos` points to line start; walk over utfucked chars
1243 while (spos < pos) {
1244 char ch = tbuf[gb.pos2real(spos)];
1245 if (!sl && ch == '\n') break;
1246 static if (dotabs) {
1247 if (ch == '\t' && visualtabs && tabsize > 0) {
1248 x = ((x+tabsize)/tabsize)*tabsize;
1249 } else {
1250 ++x;
1252 } else {
1253 ++x;
1255 spos += (ch < 128 ? 1 : gb.utfuckLenAt!true(spos));
1257 } else {
1258 // word-wrapped, eh...
1259 int lidx = findLineCacheIndex(pos);
1260 assert(lidx >= 0);
1261 int spos = locache[lidx].ofs;
1262 int epos = locache[lidx+1].ofs;
1263 assert(pos < epos);
1264 // now `spos` points to line start; walk over utfucked chars
1265 while (spos < pos) {
1266 char ch = tbuf[gb.pos2real(spos)];
1267 if (!sl && ch == '\n') break;
1268 static if (dotabs) {
1269 if (ch == '\t' && visualtabs && tabsize > 0) {
1270 x = ((x+tabsize)/tabsize)*tabsize;
1271 } else {
1272 ++x;
1274 } else {
1275 ++x;
1277 spos += (ch < 128 ? 1 : gb.utfuckLenAt!true(spos));
1280 return x;
1283 /// get position for the given text coordinates
1284 int xy2pos (int x, int y) nothrow {
1285 auto ts = gb.textsize;
1286 if (ts == 0 || y < 0) return 0;
1287 if (y > mLineCount-1) return ts;
1288 if (x < 0) x = 0;
1289 if (mLineCount == 1) {
1290 assert(y == 0);
1291 return (!utfuck ? (x < ts ? x : ts) : utfuck_x2pos(x, 0));
1293 uint ls = locache[y].ofs;
1294 uint le = locache[y+1].ofs;
1295 if (ls == le) {
1296 // this should be last empty line
1297 //if (y != mLineCount-1) { import std.format; assert(0, "fuuuuu; y=%u; lc=%u; locused=%u".format(y, mLineCount, locused)); }
1298 assert(y == mLineCount-1);
1299 return ls;
1301 if (!utfuck) {
1302 // we want line end (except for last empty line, where we want end-of-text)
1303 if (x >= le-ls) return (y != mLineCount-1 ? le-1 : le);
1304 return ls+x; // somewhere in line
1305 } else {
1306 // fuck
1307 return utfuck_x2pos(x, ls);
1311 /// get text coordinates for the given position
1312 void pos2xy (int pos, out int x, out int y) nothrow {
1313 immutable ts = gb.textsize;
1314 if (pos <= 0 || ts == 0) return; // x and y autoinited
1315 if (pos > ts) pos = ts;
1316 if (mLineCount == 1) {
1317 // y is autoinited
1318 x = (!utfuck ? pos : utfuck_pos2x(pos));
1319 return;
1321 const(char)* tbuf = gb.tbuf;
1322 if (pos == ts) {
1323 // end of text: no need to update line offset cache
1324 y = mLineCount-1;
1325 if (!gb.mSingleLine) {
1326 while (pos > 0 && tbuf[gb.pos2real(--pos)] != '\n') ++x;
1327 } else {
1328 x = pos;
1330 return;
1332 int lcidx = findLineCacheIndex(pos);
1333 assert(lcidx >= 0 && lcidx < mLineCount);
1334 immutable ls = locache[lcidx].ofs;
1335 //auto le = lineofsc[lcidx+1];
1336 //!assert(pos >= ls && pos < le);
1337 y = cast(uint)lcidx;
1338 x = (!utfuck ? pos-ls : utfuck_pos2x(pos));
1341 /// get text coordinates (adjusted for tabs) for the given position
1342 void pos2xyVT (int pos, out int x, out int y) nothrow {
1343 if (!utfuck && (!visualtabs || tabsize == 0)) { pos2xy(pos, x, y); return; }
1345 void tabbedX() (int ls) {
1346 x = 0;
1347 version(none) {
1348 //TODO:FIXME: fix this!
1349 while (ls < pos) {
1350 int tp = fastFindCharIn(ls, pos-ls, '\t');
1351 if (tp < 0) { x += pos-ls; return; }
1352 x += tp-ls;
1353 ls = tp;
1354 while (ls < pos && tbuf[pos2real(ls++)] == '\t') {
1355 x = ((x+tabsize)/tabsize)*tabsize;
1358 } else {
1359 const(char)* tbuf = gb.tbuf;
1360 while (ls < pos) {
1361 if (tbuf[gb.pos2real(ls++)] == '\t') x = ((x+tabsize)/tabsize)*tabsize; else ++x;
1366 auto ts = gb.textsize;
1367 if (pos <= 0 || ts == 0) return; // x and y autoinited
1368 if (pos > ts) pos = ts;
1369 if (mLineCount == 1) {
1370 // y is autoinited
1371 if (utfuck) { x = utfuck_pos2x!true(pos); return; }
1372 if (!visualtabs || tabsize == 0) { x = pos; return; }
1373 tabbedX(0);
1374 return;
1376 if (pos == ts) {
1377 // end of text: no need to update line offset cache
1378 const(char)* tbuf = gb.tbuf;
1379 y = mLineCount-1;
1380 while (pos > 0 && (gb.mSingleLine || tbuf[gb.pos2real(--pos)] != '\n')) ++x;
1381 if (utfuck) { x = utfuck_pos2x!true(ts); return; }
1382 if (visualtabs && tabsize != 0) { int ls = pos+1; pos = ts; tabbedX(ls); return; }
1383 return;
1385 int lcidx = findLineCacheIndex(pos);
1386 assert(lcidx >= 0 && lcidx < mLineCount);
1387 auto ls = locache[lcidx].ofs;
1388 //auto le = lineofsc[lcidx+1];
1389 //!assert(pos >= ls && pos < le);
1390 y = cast(uint)lcidx;
1391 if (utfuck) { x = utfuck_pos2x!true(pos); return; }
1392 if (visualtabs && tabsize > 0) { tabbedX(ls); return; }
1393 x = pos-ls;
1398 // ////////////////////////////////////////////////////////////////////////// //
1399 private final class UndoStack {
1400 public:
1401 enum Type : ubyte {
1402 None,
1404 CurMove, // pos: old position; len: old topline (sorry)
1405 TextRemove, // pos: position; len: length; deleted chars follows
1406 TextInsert, // pos: position; len: length
1407 // grouping
1408 GroupStart,
1409 GroupEnd,
1412 private:
1413 static align(1) struct Action {
1414 align(1):
1415 enum Flag : ubyte {
1416 BlockMarking = 1<<0, // block marking state
1417 LastBE = 1<<1, // last block move was at end?
1418 Changed = 1<<2, // "changed" flag
1419 //VisTabs = 1<<3, // editor was in "visual tabs" mode
1422 @property nothrow pure @safe @nogc {
1423 bool bmarking () const { pragma(inline, true); return (flags&Flag.BlockMarking) != 0; }
1424 bool lastbe () const { pragma(inline, true); return (flags&Flag.LastBE) != 0; }
1425 bool txchanged () const { pragma(inline, true); return (flags&Flag.Changed) != 0; }
1426 //bool vistabs () const { pragma(inline, true); return (flags&Flag.VisTabs) != 0; }
1428 void bmarking (bool v) { pragma(inline, true); if (v) flags |= Flag.BlockMarking; else flags &= ~(Flag.BlockMarking); }
1429 void lastbe (bool v) { pragma(inline, true); if (v) flags |= Flag.LastBE; else flags &= ~(Flag.LastBE); }
1430 void txchanged (bool v) { pragma(inline, true); if (v) flags |= Flag.Changed; else flags &= ~(Flag.Changed); }
1431 //void vistabs (bool v) { pragma(inline, true); if (v) flags |= Flag.VisTabs; else flags &= ~Flag.VisTabs; }
1434 Type type;
1435 int pos;
1436 int len;
1437 // after undoing action
1438 int cx, cy, topline, xofs;
1439 int bs, be; // block position
1440 ubyte flags;
1441 // data follows
1442 char[0] data;
1445 version(Posix) private import core.sys.posix.unistd : off_t;
1447 private:
1448 version(Posix) int tmpfd = -1; else enum tmpfd = -1;
1449 version(Posix) off_t tmpsize = 0;
1450 bool asRedo;
1451 // undo buffer format:
1452 // last uint is always record size (not including size uints); record follows (up), then size again
1453 uint maxBufSize = 32*1024*1024;
1454 ubyte* undoBuffer;
1455 uint ubUsed, ubSize;
1456 bool asRich;
1458 final:
1459 version(Posix) void initTempFD () nothrow {
1460 import core.sys.posix.fcntl /*: open*/;
1461 static if (is(typeof(O_CLOEXEC)) && is(typeof(O_TMPFILE))) {
1462 auto xfd = open("/tmp/_egundoz", O_RDWR|O_CLOEXEC|O_TMPFILE, 0x1b6/*0o600*/);
1463 if (xfd < 0) return;
1464 tmpfd = xfd;
1465 tmpsize = 0;
1469 // returns record size
1470 version(Posix) uint loadLastRecord(bool fullrecord=true) (bool dropit=false) nothrow {
1471 import core.stdc.stdio : SEEK_SET, SEEK_END;
1472 import core.sys.posix.unistd : lseek, read;
1473 assert(tmpfd >= 0);
1474 uint sz;
1475 if (tmpsize < sz.sizeof) return 0;
1476 lseek(tmpfd, tmpsize-sz.sizeof, SEEK_SET);
1477 if (read(tmpfd, &sz, sz.sizeof) != sz.sizeof) return 0;
1478 if (tmpsize < sz+sz.sizeof*2) return 0;
1479 if (sz < Action.sizeof) return 0;
1480 lseek(tmpfd, tmpsize-sz-sz.sizeof, SEEK_SET);
1481 static if (fullrecord) {
1482 alias rsz = sz;
1483 } else {
1484 auto rsz = cast(uint)Action.sizeof;
1486 if (ubSize < rsz) {
1487 import core.stdc.stdlib : realloc;
1488 auto nb = cast(ubyte*)realloc(undoBuffer, rsz);
1489 if (nb is null) return 0;
1490 undoBuffer = nb;
1491 ubSize = rsz;
1493 ubUsed = rsz;
1494 if (read(tmpfd, undoBuffer, rsz) != rsz) return 0;
1495 if (dropit) tmpsize -= sz+sz.sizeof*2;
1496 return rsz;
1499 bool saveLastRecord () nothrow {
1500 version(Posix) {
1501 import core.stdc.stdio : SEEK_SET;
1502 import core.sys.posix.unistd : lseek, write;
1503 if (tmpfd >= 0) {
1504 assert(ubUsed >= Action.sizeof);
1505 scope(exit) {
1506 import core.stdc.stdlib : free;
1507 if (ubUsed > 65536) {
1508 free(undoBuffer);
1509 undoBuffer = null;
1510 ubUsed = ubSize = 0;
1513 auto ofs = lseek(tmpfd, tmpsize, SEEK_SET);
1514 if (write(tmpfd, &ubUsed, ubUsed.sizeof) != ubUsed.sizeof) return false;
1515 if (write(tmpfd, undoBuffer, ubUsed) != ubUsed) return false;
1516 if (write(tmpfd, &ubUsed, ubUsed.sizeof) != ubUsed.sizeof) return false;
1517 write(tmpfd, &tmpsize, tmpsize.sizeof);
1518 tmpsize += ubUsed+uint.sizeof*2;
1521 return true;
1524 // return `true` if something was removed
1525 bool removeFirstUndo () nothrow {
1526 import core.stdc.string : memmove;
1527 version(Posix) assert(tmpfd < 0);
1528 if (ubUsed == 0) return false;
1529 uint np = (*cast(uint*)undoBuffer)+4*2;
1530 assert(np <= ubUsed);
1531 if (np == ubUsed) { ubUsed = 0; return true; }
1532 memmove(undoBuffer, undoBuffer+np, ubUsed-np);
1533 ubUsed -= np;
1534 return true;
1537 // return `null` if it can't; undo buffer is in invalid state then
1538 Action* addUndo (int dataSize) nothrow {
1539 import core.stdc.stdlib : realloc;
1540 import core.stdc.string : memset;
1541 version(Posix) if (tmpfd < 0) {
1542 if (dataSize < 0 || dataSize >= maxBufSize) return null; // no room
1543 uint asz = cast(uint)Action.sizeof+dataSize+4*2;
1544 if (asz > maxBufSize) return null;
1545 if (ubSize-ubUsed < asz) {
1546 uint nasz = ubUsed+asz;
1547 if (nasz&0xffff) nasz = (nasz|0xffff)+1;
1548 if (nasz > maxBufSize) {
1549 while (ubSize-ubUsed < asz) { if (!removeFirstUndo()) return null; }
1550 } else {
1551 auto nb = cast(ubyte*)realloc(undoBuffer, nasz);
1552 if (nb is null) {
1553 while (ubSize-ubUsed < asz) { if (!removeFirstUndo()) return null; }
1554 } else {
1555 undoBuffer = nb;
1556 ubSize = nasz;
1560 assert(ubSize-ubUsed >= asz);
1561 *cast(uint*)(undoBuffer+ubUsed) = asz-4*2;
1562 auto res = cast(Action*)(undoBuffer+ubUsed+4);
1563 *cast(uint*)(undoBuffer+ubUsed+asz-4) = asz-4*2;
1564 ubUsed += asz;
1565 memset(res, 0, asz-4*2);
1566 return res;
1569 // has temp file
1570 if (dataSize < 0 || dataSize >= int.max/4) return null; // wtf?!
1571 uint asz = cast(uint)Action.sizeof+dataSize;
1572 if (ubSize < asz) {
1573 auto nb = cast(ubyte*)realloc(undoBuffer, asz);
1574 if (nb is null) return null;
1575 undoBuffer = nb;
1576 ubSize = asz;
1578 ubUsed = asz;
1579 auto res = cast(Action*)undoBuffer;
1580 memset(res, 0, asz);
1581 return res;
1585 // can return null
1586 Action* lastUndoHead () nothrow {
1587 version(Posix) if (tmpfd >= 0) {
1588 if (loadLastRecord!false()) return null;
1589 return cast(Action*)undoBuffer;
1592 if (ubUsed == 0) return null;
1593 auto sz = *cast(uint*)(undoBuffer+ubUsed-4);
1594 return cast(Action*)(undoBuffer+ubUsed-4-sz);
1598 Action* popUndo () nothrow {
1599 version(Posix) if (tmpfd >= 0) {
1600 auto len = loadLastRecord!true(true); // pop it
1601 return (len ? cast(Action*)undoBuffer : null);
1604 if (ubUsed == 0) return null;
1605 auto sz = *cast(uint*)(undoBuffer+ubUsed-4);
1606 auto res = cast(Action*)(undoBuffer+ubUsed-4-sz);
1607 ubUsed -= sz+4*2;
1608 return res;
1612 public:
1613 this (bool aAsRich, bool aAsRedo, bool aIntoFile) nothrow {
1614 asRedo = aAsRedo;
1615 asRich = aAsRich;
1616 if (aIntoFile) {
1617 initTempFD();
1618 if (tmpfd < 0) {
1619 //version(aliced) { import iv.rawtty; ttyBeep(); }
1624 ~this () nothrow {
1625 import core.stdc.stdlib : free;
1626 import core.sys.posix.unistd : close;
1627 if (tmpfd >= 0) { close(tmpfd); tmpfd = -1; }
1628 if (undoBuffer !is null) free(undoBuffer);
1631 void clear (bool doclose=false) nothrow {
1632 ubUsed = 0;
1633 if (doclose) {
1634 version(Posix) {
1635 import core.stdc.stdlib : free;
1636 if (tmpfd >= 0) {
1637 import core.sys.posix.unistd : close;
1638 close(tmpfd);
1639 tmpfd = -1;
1642 if (undoBuffer !is null) free(undoBuffer);
1643 undoBuffer = null;
1644 ubSize = 0;
1645 } else {
1646 if (ubSize > 65536) {
1647 import core.stdc.stdlib : realloc;
1648 auto nb = cast(ubyte*)realloc(undoBuffer, 65536);
1649 if (nb !is null) {
1650 undoBuffer = nb;
1651 ubSize = 65536;
1654 version(Posix) if (tmpfd >= 0) tmpsize = 0;
1658 void alwaysChanged () nothrow {
1659 if (tmpfd < 0) {
1660 auto pos = 0;
1661 while (pos < ubUsed) {
1662 auto sz = *cast(uint*)(undoBuffer+pos);
1663 auto res = cast(Action*)(undoBuffer+pos+4);
1664 pos += sz+4*2;
1665 switch (res.type) {
1666 case Type.TextRemove:
1667 case Type.TextInsert:
1668 res.txchanged = true;
1669 break;
1670 default:
1673 } else {
1674 version(Posix) {
1675 import core.stdc.stdio : SEEK_SET;
1676 import core.sys.posix.unistd : lseek, read, write;
1677 off_t cpos = 0;
1678 Action act;
1679 while (cpos < tmpsize) {
1680 uint sz;
1681 lseek(tmpfd, cpos, SEEK_SET);
1682 if (read(tmpfd, &sz, sz.sizeof) != sz.sizeof) break;
1683 if (sz < Action.sizeof) assert(0, "wtf?!");
1684 if (read(tmpfd, &act, Action.sizeof) != Action.sizeof) break;
1685 switch (act.type) {
1686 case Type.TextRemove:
1687 case Type.TextInsert:
1688 if (act.txchanged != true) {
1689 act.txchanged = true;
1690 lseek(tmpfd, cpos+sz.sizeof, SEEK_SET);
1691 write(tmpfd, &act, Action.sizeof);
1693 break;
1694 default:
1696 cpos += sz+sz.sizeof*2;
1702 private void fillCurPos (Action* ua, EditorEngine ed) nothrow {
1703 if (ua !is null && ed !is null) {
1704 //TODO: correct x according to "visual tabs" mode (i.e. make it "normal x")
1705 ua.cx = ed.cx;
1706 ua.cy = ed.cy;
1707 ua.topline = ed.mTopLine;
1708 ua.xofs = ed.mXOfs;
1709 ua.bs = ed.bstart;
1710 ua.be = ed.bend;
1711 ua.bmarking = ed.markingBlock;
1712 ua.lastbe = ed.lastBGEnd;
1713 ua.txchanged = ed.txchanged;
1714 //ua.vistabs = ed.visualtabs;
1718 bool addCurMove (EditorEngine ed, bool fromRedo=false) nothrow {
1719 if (auto lu = lastUndoHead()) {
1720 if (lu.type == Type.CurMove) {
1721 if (lu.cx == ed.cx && lu.cy == ed.cy && lu.topline == ed.mTopLine && lu.xofs == ed.mXOfs &&
1722 lu.bs == ed.bstart && lu.be == ed.bend && lu.bmarking == ed.markingBlock &&
1723 lu.lastbe == ed.lastBGEnd /*&& lu.vistabs == ed.visualtabs*/) return true;
1726 if (!asRedo && !fromRedo && ed.redo !is null) ed.redo.clear();
1727 auto act = addUndo(0);
1728 if (act is null) { clear(); return false; }
1729 act.type = Type.CurMove;
1730 fillCurPos(act, ed);
1731 return saveLastRecord();
1734 bool addTextRemove (EditorEngine ed, int pos, int count, bool fromRedo=false) nothrow {
1735 if (!asRedo && !fromRedo && ed.redo !is null) ed.redo.clear();
1736 GapBuffer gb = ed.gb;
1737 assert(gb !is null);
1738 if (pos < 0 || pos >= gb.textsize) return true;
1739 if (count < 1) return true;
1740 if (count >= maxBufSize) { clear(); return false; }
1741 if (count > gb.textsize-pos) { clear(); return false; }
1742 int realcount = count;
1743 if (asRich && realcount > 0) {
1744 if (realcount >= int.max/gb.hbuf[0].sizeof/2) return false;
1745 realcount += realcount*cast(int)gb.hbuf[0].sizeof;
1747 auto act = addUndo(realcount);
1748 if (act is null) { clear(); return false; }
1749 act.type = Type.TextRemove;
1750 act.pos = pos;
1751 act.len = count;
1752 fillCurPos(act, ed);
1753 auto dp = act.data.ptr;
1754 while (count--) *dp++ = gb[pos++];
1755 // save attrs for rich editor
1756 if (asRich && realcount > 0) {
1757 pos = act.pos;
1758 count = act.len;
1759 auto dph = cast(GapBuffer.HighState*)dp;
1760 while (count--) *dph++ = gb.hi(pos++);
1762 return saveLastRecord();
1765 bool addTextInsert (EditorEngine ed, int pos, int count, bool fromRedo=false) nothrow {
1766 if (!asRedo && !fromRedo && ed.redo !is null) ed.redo.clear();
1767 auto act = addUndo(0);
1768 if (act is null) { clear(); return false; }
1769 act.type = Type.TextInsert;
1770 act.pos = pos;
1771 act.len = count;
1772 fillCurPos(act, ed);
1773 return saveLastRecord();
1776 bool addGroupStart (EditorEngine ed, bool fromRedo=false) nothrow {
1777 if (!asRedo && !fromRedo && ed.redo !is null) ed.redo.clear();
1778 auto act = addUndo(0);
1779 if (act is null) { clear(); return false; }
1780 act.type = Type.GroupStart;
1781 fillCurPos(act, ed);
1782 return saveLastRecord();
1785 bool addGroupEnd (EditorEngine ed, bool fromRedo=false) nothrow {
1786 if (!asRedo && !fromRedo && ed.redo !is null) ed.redo.clear();
1787 auto act = addUndo(0);
1788 if (act is null) { clear(); return false; }
1789 act.type = Type.GroupEnd;
1790 fillCurPos(act, ed);
1791 return saveLastRecord();
1794 @property bool hasUndo () const pure nothrow @safe @nogc { pragma(inline, true); return (tmpfd < 0 ? (ubUsed > 0) : (tmpsize > 0)); }
1796 private bool copyAction (Action* ua) nothrow {
1797 import core.stdc.string : memcpy;
1798 if (ua is null) return true;
1799 auto na = addUndo(ua.type == Type.TextRemove ? ua.len : 0);
1800 if (na is null) return false;
1801 memcpy(na, ua, Action.sizeof+(ua.type == Type.TextRemove ? ua.len : 0));
1802 return saveLastRecord();
1805 // return "None" in case of error
1806 Type undoAction (EditorEngine ed) {
1807 UndoStack oppos = (asRedo ? ed.undo : ed.redo);
1808 assert(ed !is null);
1809 auto ua = popUndo();
1810 if (ua is null) return Type.None;
1811 //debug(egauto) if (!asRedo) { { import iv.vfs; auto fo = VFile("z00_undo.bin", "a"); fo.writeln(*ua); } }
1812 Type res = ua.type;
1813 final switch (ua.type) {
1814 case Type.None: assert(0, "wtf?!");
1815 case Type.GroupStart:
1816 case Type.GroupEnd:
1817 if (oppos !is null) { if (!oppos.copyAction(ua)) oppos.clear(); }
1818 break;
1819 case Type.CurMove:
1820 if (oppos !is null) { if (oppos.addCurMove(ed, asRedo) == Type.None) oppos.clear(); }
1821 break;
1822 case Type.TextInsert: // remove inserted text
1823 if (oppos !is null) { if (oppos.addTextRemove(ed, ua.pos, ua.len, asRedo) == Type.None) oppos.clear(); }
1824 //ed.writeLogAction(ua.pos, -ua.len);
1825 ed.ubTextRemove(ua.pos, ua.len);
1826 break;
1827 case Type.TextRemove: // insert removed text
1828 if (oppos !is null) { if (oppos.addTextInsert(ed, ua.pos, ua.len, asRedo) == Type.None) oppos.clear(); }
1829 if (ed.ubTextInsert(ua.pos, ua.data.ptr[0..ua.len])) {
1830 if (asRich) ed.ubTextSetAttrs(ua.pos, (cast(GapBuffer.HighState*)(ua.data.ptr+ua.len))[0..ua.len]);
1832 //ed.writeLogAction(ua.pos, ua.len);
1833 break;
1835 //FIXME: optimize redraw
1836 if (ua.bs != ed.bstart || ua.be != ed.bend) {
1837 if (ua.bs < ua.be) {
1838 // undo has block
1839 if (ed.bstart < ed.bend) ed.markLinesDirtySE(ed.lc.pos2line(ed.bstart), ed.lc.pos2line(ed.bend)); // old block is dirty
1840 ed.markLinesDirtySE(ed.lc.pos2line(ua.bs), ed.lc.pos2line(ua.be)); // new block is dirty
1841 } else {
1842 // undo has no block
1843 if (ed.bstart < ed.bend) ed.markLinesDirtySE(ed.lc.pos2line(ed.bstart), ed.lc.pos2line(ed.bend)); // old block is dirty
1846 ed.bstart = ua.bs;
1847 ed.bend = ua.be;
1848 ed.markingBlock = ua.bmarking;
1849 ed.lastBGEnd = ua.lastbe;
1850 // don't restore "visual tabs" mode
1851 //TODO: correct x according to "visual tabs" mode (i.e. make it "visual x")
1852 ed.cx = ua.cx;
1853 ed.cy = ua.cy;
1854 ed.mTopLine = ua.topline;
1855 ed.mXOfs = ua.xofs;
1856 ed.txchanged = ua.txchanged;
1857 return res;
1862 // ////////////////////////////////////////////////////////////////////////// //
1863 /// main editor engine: does all the undo/redo magic, block management, etc.
1864 class EditorEngine {
1865 public:
1867 enum CodePage : ubyte {
1868 koi8u, ///
1869 cp1251, ///
1870 cp866, ///
1872 CodePage codepage = CodePage.koi8u; ///
1874 /// from koi to codepage
1875 final char recodeCharTo (char ch) pure const nothrow {
1876 pragma(inline, true);
1877 return
1878 codepage == CodePage.cp1251 ? uni2cp1251(koi2uni(ch)) :
1879 codepage == CodePage.cp866 ? uni2cp866(koi2uni(ch)) :
1883 /// from codepage to koi
1884 final char recodeCharFrom (char ch) pure const nothrow {
1885 pragma(inline, true);
1886 return
1887 codepage == CodePage.cp1251 ? uni2koi(cp12512uni(ch)) :
1888 codepage == CodePage.cp866 ? uni2koi(cp8662uni(ch)) :
1892 /// recode to codepage
1893 final char recodeU2B (dchar dch) pure const nothrow {
1894 final switch (codepage) {
1895 case CodePage.koi8u: return uni2koi(dch);
1896 case CodePage.cp1251: return uni2cp1251(dch);
1897 case CodePage.cp866: return uni2cp866(dch);
1901 /// should not be called for utfuck mode
1902 final dchar recode1b (char ch) pure const nothrow {
1903 final switch (codepage) {
1904 case CodePage.koi8u: return koi2uni(ch);
1905 case CodePage.cp1251: return cp12512uni(ch);
1906 case CodePage.cp866: return cp8662uni(ch);
1910 protected:
1911 int lineHeightPixels = 0; /// <0: use line height API, proportional fonts; 0: everything is cell-based (tty); >0: constant line height in pixels; proprotional fonts
1912 int prevTopLine = -1;
1913 int mTopLine = 0;
1914 int prevXOfs = -1;
1915 int mXOfs = 0;
1916 int cx, cy;
1917 int[] dirtyLines; // line heights or 0 if not dirty; hack!
1918 int winx, winy, winw, winh;
1919 GapBuffer gb;
1920 LineCache lc;
1921 EditorHL hl;
1922 UndoStack undo, redo;
1923 int bstart = -1, bend = -1; // marked block position
1924 bool markingBlock;
1925 bool lastBGEnd; // last block grow was at end?
1926 bool txchanged;
1927 bool mReadOnly; // has any effect only if you are using `insertText()` and `deleteText()` API!
1928 bool mSingleLine; // has any effect only if you are using `insertText()` and `deleteText()` API!
1929 bool mKillTextOnChar; // mostly for single-line: remove all text on new char; will autoreset on move
1931 char[] indentText; // this buffer is actively reused, do not expose!
1932 int inPasteMode;
1934 bool mAsRich; /// this is "rich editor", so engine should save/restore highlighting info in undo
1936 protected:
1937 bool[int] linebookmarked; /// is this line bookmarked?
1939 public:
1940 //EgTextMeter textMeter; /// *MUST* be set when `inPixels` is true
1941 final @property EgTextMeter textMeter () nothrow @nogc { return lc.textMeter; }
1942 final @property void textMeter (EgTextMeter tm) nothrow @nogc { lc.textMeter = tm; }
1944 final @property int wordWrapPos () const nothrow @nogc { pragma(inline, true); return lc.mWordWrapWidth; }
1945 final @property void wordWrapPos (int v) nothrow {
1946 if (v < 0) v = 0;
1947 if (lc.mWordWrapWidth != v) {
1948 auto pos = curpos;
1949 lc.mWordWrapWidth = v;
1950 lc.rebuild();
1951 gotoPos!true(pos);
1955 public:
1956 /// is editor in "paste mode" (i.e. we are pasting chars from clipboard, and should skip autoindenting)?
1957 final @property bool pasteMode () const pure nothrow @safe @nogc { return (inPasteMode > 0); }
1958 final resetPasteMode () pure nothrow @safe @nogc { inPasteMode = 0; } /// reset "paste mode"
1961 void clearBookmarks () nothrow { linebookmarked.clear(); }
1963 enum BookmarkChangeMode { Toggle, Set, Reset } ///
1966 void bookmarkChange (int cy, BookmarkChangeMode mode) nothrow {
1967 if (cy < 0 || cy >= lc.linecount) return;
1968 if (mSingleLine) return; // ignore for single-line mode
1969 final switch (mode) {
1970 case BookmarkChangeMode.Toggle:
1971 if (cy in linebookmarked) linebookmarked.remove(cy); else linebookmarked[cy] = true;
1972 markLinesDirty(cy, 1);
1973 break;
1974 case BookmarkChangeMode.Set:
1975 if (cy !in linebookmarked) {
1976 linebookmarked[cy] = true;
1977 markLinesDirty(cy, 1);
1979 break;
1980 case BookmarkChangeMode.Reset:
1981 if (cy in linebookmarked) {
1982 linebookmarked.remove(cy);
1983 markLinesDirty(cy, 1);
1985 break;
1990 final void doBookmarkToggle () nothrow { pragma(inline, true); bookmarkChange(cy, BookmarkChangeMode.Toggle); }
1993 final @property bool isLineBookmarked (int lidx) nothrow {
1994 pragma(inline, true);
1995 return ((lidx in linebookmarked) !is null);
1999 final void doBookmarkJumpUp () nothrow {
2000 int bestBM = -1;
2001 foreach (int lidx; linebookmarked.byKey) {
2002 if (lidx < cy && lidx > bestBM) bestBM = lidx;
2004 if (bestBM >= 0) {
2005 pushUndoCurPos();
2006 cy = bestBM;
2007 normXY;
2008 growBlockMark();
2009 makeCurLineVisibleCentered();
2014 final void doBookmarkJumpDown () nothrow {
2015 int bestBM = int.max;
2016 foreach (int lidx; linebookmarked.byKey) {
2017 if (lidx > cy && lidx < bestBM) bestBM = lidx;
2019 if (bestBM < lc.linecount) {
2020 pushUndoCurPos();
2021 cy = bestBM;
2022 normXY;
2023 growBlockMark();
2024 makeCurLineVisibleCentered();
2028 ///WARNING! don't mutate bookmarks here!
2029 final void forEachBookmark (scope void delegate (int lidx) dg) {
2030 if (dg is null) return;
2031 foreach (int lidx; linebookmarked.byKey) dg(lidx);
2034 /// call this from `willBeDeleted()` (only!) to fix bookmarks
2035 final void bookmarkDeletionFix (int pos, int len, int eolcount) nothrow {
2036 if (eolcount && linebookmarked.length > 0) {
2037 import core.stdc.stdlib : malloc, free;
2038 // remove bookmarks whose lines are removed, move other bookmarks
2039 auto py = lc.pos2line(pos);
2040 auto ey = lc.pos2line(pos+len);
2041 bool wholeFirstLineDeleted = (pos == lc.line2pos(py)); // do we want to remove the whole first line?
2042 bool wholeLastLineDeleted = (pos+len == lc.line2pos(ey)); // do we want to remove the whole last line?
2043 if (wholeLastLineDeleted) --ey; // yes, `ey` is one line down the last, fix it
2044 // build new bookmark array
2045 int* newbm = cast(int*)malloc(int.sizeof*linebookmarked.length);
2046 if (newbm !is null) {
2047 scope(exit) free(newbm);
2048 int newbmpos = 0;
2049 bool smthWasChanged = false;
2050 foreach (int lidx; linebookmarked.byKey) {
2051 // remove "first line" bookmark if "first line" is deleted
2052 if (wholeFirstLineDeleted && lidx == py) { smthWasChanged = true; continue; }
2053 // remove "last line" bookmark if "last line" is deleted
2054 if (wholeLastLineDeleted && lidx == ey) { smthWasChanged = true; continue; }
2055 // remove bookmarks that are in range
2056 if (lidx > py && lidx < ey) continue;
2057 // fix bookmark line if necessary
2058 if (lidx >= ey) { smthWasChanged = true; lidx -= eolcount; }
2059 if (lidx >= 0 && lidx < lc.linecount) {
2060 //assert(lidx >= 0 && lidx < lc.linecount);
2061 // add this bookmark to new list
2062 newbm[newbmpos++] = lidx;
2065 // rebuild list if something was changed
2066 if (smthWasChanged) {
2067 fullDirty(); //TODO: optimize this
2068 linebookmarked.clear;
2069 foreach (int lidx; newbm[0..newbmpos]) linebookmarked[lidx] = true;
2071 } else {
2072 // out of memory, what to do? just clear bookmarks for now
2073 linebookmarked.clear;
2074 fullDirty(); // just in case
2079 /// call this from `willBeInserted()` or `wasInserted()` to fix bookmarks
2080 final void bookmarkInsertionFix (int pos, int len, int eolcount) nothrow {
2081 if (eolcount && linebookmarked.length > 0) {
2082 import core.stdc.stdlib : malloc, free;
2083 // move affected bookmarks down
2084 auto py = lc.pos2line(pos);
2085 if (pos != lc.line2pos(py)) ++py; // not the whole first line was modified, don't touch bookmarks on it
2086 // build new bookmark array
2087 int* newbm = cast(int*)malloc(int.sizeof*linebookmarked.length);
2088 if (newbm !is null) {
2089 scope(exit) free(newbm);
2090 int newbmpos = 0;
2091 bool smthWasChanged = false;
2092 foreach (int lidx; linebookmarked.byKey) {
2093 // fix bookmark line if necessary
2094 if (lidx >= py) { smthWasChanged = true; lidx += eolcount; }
2095 if (lidx < 0 || lidx >= lc.linecount) continue;
2096 //assert(lidx >= 0 && lidx < gb.linecount);
2097 // add this bookmark to new list
2098 newbm[newbmpos++] = lidx;
2100 // rebuild list if something was changed
2101 if (smthWasChanged) {
2102 fullDirty(); //TODO: optimize this
2103 linebookmarked.clear;
2104 foreach (int lidx; newbm[0..newbmpos]) linebookmarked[lidx] = true;
2106 } else {
2107 // out of memory, what to do? just clear bookmarks for now
2108 linebookmarked.clear;
2109 fullDirty(); // just in case
2114 public:
2116 this (int x0, int y0, int w, int h, EditorHL ahl=null, bool asingleline=false) {
2117 if (w < 2) w = 2;
2118 if (h < 1) h = 1;
2119 winx = x0;
2120 winy = y0;
2121 winw = w;
2122 winh = h;
2123 //setDirtyLinesLength(visibleLinesPerWindow);
2124 gb = new GapBuffer(asingleline);
2125 lc = new LineCache(gb);
2126 lc.recode1byte = &recode1b;
2127 hl = ahl;
2128 if (ahl !is null) { hl.gb = gb; hl.lc = lc; }
2129 undo = new UndoStack(mAsRich, false, !asingleline);
2130 redo = new UndoStack(mAsRich, true, !asingleline);
2131 mSingleLine = asingleline;
2134 private void setDirtyLinesLength (usize len) nothrow {
2135 if (len > int.max/4) assert(0, "wtf?!");
2136 if (dirtyLines.length > len) {
2137 dirtyLines.length = len;
2138 dirtyLines.assumeSafeAppend;
2139 dirtyLines[] = -1;
2140 } else if (dirtyLines.length < len) {
2141 auto optr = dirtyLines.ptr;
2142 auto olen = dirtyLines.length;
2143 dirtyLines.length = len;
2144 if (dirtyLines.ptr !is optr) {
2145 import core.memory : GC;
2146 if (dirtyLines.ptr is GC.addrOf(dirtyLines.ptr)) GC.setAttr(dirtyLines.ptr, GC.BlkAttr.NO_INTERIOR);
2148 //dirtyLines[olen..$] = -1;
2149 dirtyLines[] = -1;
2153 // utfuck switch hooks
2154 protected void beforeUtfuckSwitch (bool newisutfuck) {} /// utfuck switch hook
2155 protected void afterUtfuckSwitch (bool newisutfuck) {} /// utfuck switch hook
2157 final @property {
2159 bool utfuck () const pure nothrow @safe @nogc { pragma(inline, true); return lc.utfuck; }
2161 /// this switches "utfuck" mode
2162 /// note that utfuck mode is FUCKIN' SLOW and buggy
2163 /// you should not lose any text, but may encounter visual and positional glitches
2164 void utfuck (bool v) {
2165 if (lc.utfuck == v) return;
2166 beforeUtfuckSwitch(v);
2167 auto pos = curpos;
2168 lc.utfuck = v;
2169 lc.pos2xy(pos, cx, cy);
2170 fullDirty();
2171 afterUtfuckSwitch(v);
2174 ref inout(GapBuffer.HighState) defaultRichStyle () inout pure nothrow @trusted @nogc { pragma(inline, true); return cast(typeof(return))gb.defhs; } ///
2176 @property bool asRich () const pure nothrow @safe @nogc { pragma(inline, true); return mAsRich; } ///
2178 /// WARNING! changing this will reset undo/redo buffers!
2179 void asRich (bool v) {
2180 if (mAsRich != v) {
2181 // detach highlighter for "rich mode"
2182 if (v && hl !is null) {
2183 hl.gb = null;
2184 hl.lc = null;
2185 hl = null;
2187 mAsRich = v;
2188 if (undo !is null) {
2189 delete undo;
2190 undo = new UndoStack(mAsRich, false, !singleline);
2192 if (redo !is null) {
2193 delete redo;
2194 redo = new UndoStack(mAsRich, true, !singleline);
2196 gb.hasHiBuffer = v; // "rich" mode require highlighting buffer, normal mode doesn't, as it has no highlighter
2197 if (v && !gb.hasHiBuffer) assert(0, "out of memory"); // alas
2201 @property bool hasHiBuffer () const pure nothrow @safe @nogc { pragma(inline, true); return gb.hasHiBuffer; }
2202 @property void hasHiBuffer (bool v) nothrow @trusted @nogc {
2203 if (mAsRich) return; // cannot change
2204 if (hl !is null) return; // cannot change too
2205 gb.hasHiBuffer = v; // otherwise it is ok to change it
2208 int x0 () const pure nothrow @safe @nogc { pragma(inline, true); return winx; } ///
2209 int y0 () const pure nothrow @safe @nogc { pragma(inline, true); return winy; } ///
2210 int width () const pure nothrow @safe @nogc { pragma(inline, true); return winw; } ///
2211 int height () const pure nothrow @safe @nogc { pragma(inline, true); return winh; } ///
2213 void x0 (int v) { pragma(inline, true); move(v, winy); } ///
2214 void y0 (int v) { pragma(inline, true); move(winx, v); } ///
2215 void width (int v) { pragma(inline, true); resize(v, winh); } ///
2216 void height (int v) { pragma(inline, true); resize(winw, v); } ///
2218 /// has any effect only if you are using `insertText()` and `deleteText()` API!
2219 bool readonly () const pure nothrow @safe @nogc { pragma(inline, true); return mReadOnly; }
2220 void readonly (bool v) nothrow { pragma(inline, true); mReadOnly = v; } ///
2222 /// "single line" mode, for line editors
2223 bool singleline () const pure nothrow @safe @nogc { pragma(inline, true); return mSingleLine; }
2225 /// "buffer change counter"
2226 uint bufferCC () const pure nothrow @safe @nogc { pragma(inline, true); return gb.bufferChangeCounter; }
2227 void bufferCC (uint v) pure nothrow { pragma(inline, true); gb.bufferChangeCounter = v; } ///
2229 bool killTextOnChar () const pure nothrow @safe @nogc { pragma(inline, true); return mKillTextOnChar; } ///
2230 void killTextOnChar (bool v) nothrow { ///
2231 pragma(inline, true);
2232 if (mKillTextOnChar != v) {
2233 mKillTextOnChar = v;
2234 fullDirty();
2238 bool inPixels () const pure nothrow @safe @nogc { pragma(inline, true); return (lineHeightPixels != 0); } ///
2240 /// this can recalc height cache
2241 int linesPerWindow () nothrow {
2242 pragma(inline, true);
2243 return
2244 lineHeightPixels == 0 || lineHeightPixels == 1 ? winh :
2245 lineHeightPixels > 0 ? (winh <= lineHeightPixels ? 1 : winh/lineHeightPixels) :
2246 calcLinesPerWindow();
2249 /// this can recalc height cache
2250 int visibleLinesPerWindow () nothrow {
2251 pragma(inline, true);
2252 return
2253 lineHeightPixels == 0 || lineHeightPixels == 1 ? winh :
2254 lineHeightPixels > 0 ? (winh <= lineHeightPixels ? 1 : winh/lineHeightPixels+(winh%lineHeightPixels ? 1 : 0)) :
2255 calcVisLinesPerWindow();
2259 // for variable line height
2260 protected final int calcVisLinesPerWindow () nothrow {
2261 if (textMeter is null) assert(0, "you forgot to setup `textMeter` for EditorEngine");
2262 int hgtleft = winh;
2263 if (hgtleft < 1) return 1; // just in case
2264 int lidx = mTopLine;
2265 int lcount = 0;
2266 while (hgtleft > 0) {
2267 auto lh = lc.lineHeightPixels(lidx++);
2268 ++lcount;
2269 hgtleft -= lh;
2271 return lcount;
2274 // for variable line height
2275 protected final int calcLinesPerWindow () nothrow {
2276 if (textMeter is null) assert(0, "you forgot to setup `textMeter` for EditorEngine");
2277 int hgtleft = winh;
2278 if (hgtleft < 1) return 1; // just in case
2279 int lidx = mTopLine;
2280 int lcount = 0;
2281 //{ import core.stdc.stdio; printf("=== clpw ===\n"); }
2282 for (;;) {
2283 auto lh = lc.lineHeightPixels(lidx++);
2284 //if (gb.mLineCount > 0) { import core.stdc.stdio; printf("*clpw: lidx=%d; height=%d; hgtleft=%d\n", lidx-1, lh, hgtleft); }
2285 hgtleft -= lh;
2286 if (hgtleft >= 0) ++lcount;
2287 if (hgtleft <= 0) break;
2289 //{ import core.stdc.stdio; printf("clpw: %d\n", lcount); }
2290 return (lcount ? lcount : 1);
2293 /// has lille sense if `inPixels` is false
2294 final int linePixelHeight (int lidx) nothrow {
2295 if (!inPixels) return 1;
2296 if (lineHeightPixels > 0) {
2297 return lineHeightPixels;
2298 } else {
2299 if (textMeter is null) assert(0, "you forgot to setup `textMeter` for EditorEngine");
2300 return lc.lineHeightPixels(lidx);
2304 /// resize control
2305 void resize (int nw, int nh) {
2306 if (nw < 2) nw = 2;
2307 if (nh < 1) nh = 1;
2308 if (nw != winw || nh != winh) {
2309 winw = nw;
2310 winh = nh;
2311 auto nvl = visibleLinesPerWindow;
2312 setDirtyLinesLength(nvl);
2313 makeCurLineVisible();
2314 fullDirty();
2318 /// move control
2319 void move (int nx, int ny) {
2320 if (winx != nx || winy != ny) {
2321 winx = nx;
2322 winy = ny;
2323 fullDirty();
2327 /// move and resize control
2328 void moveResize (int nx, int ny, int nw, int nh) {
2329 move(nx, ny);
2330 resize(nw, nh);
2333 final @property void curx (int v) nothrow @system { gotoXY(v, cy); } ///
2334 final @property void cury (int v) nothrow @system { gotoXY(cx, v); } ///
2336 final @property nothrow {
2337 /// has active marked block?
2338 bool hasMarkedBlock () const pure @safe @nogc { pragma(inline, true); return (bstart < bend); }
2340 int curx () const pure @safe @nogc { pragma(inline, true); return cx; } ///
2341 int cury () const pure @safe @nogc { pragma(inline, true); return cy; } ///
2342 int xofs () const pure @safe @nogc { pragma(inline, true); return mXOfs; } ///
2344 int topline () const pure @safe @nogc { pragma(inline, true); return mTopLine; } ///
2345 int linecount () const pure @safe @nogc { pragma(inline, true); return lc.linecount; } ///
2346 int textsize () const pure @safe @nogc { pragma(inline, true); return gb.textsize; } ///
2348 char opIndex (int pos) const pure @safe @nogc { pragma(inline, true); return gb[pos]; } /// returns '\n' for out-of-bounds query
2350 /// returns '\n' for out-of-bounds query
2351 dchar dcharAt (int pos) const pure {
2352 auto ts = gb.textsize;
2353 if (pos < 0 || pos >= ts) return '\n';
2354 if (!lc.utfuck) {
2355 final switch (codepage) {
2356 case CodePage.koi8u: return koi2uni(gb[pos]);
2357 case CodePage.cp1251: return cp12512uni(gb[pos]);
2358 case CodePage.cp866: return cp8662uni(gb[pos]);
2360 assert(0);
2362 Utf8DecoderFast udc;
2363 while (pos < ts) {
2364 if (udc.decodeSafe(cast(ubyte)gb[pos++])) return cast(dchar)udc.codepoint;
2366 return udc.replacement;
2369 /// this advances `pos`, and returns '\n' for out-of-bounds query
2370 dchar dcharAtAdvance (ref int pos) const pure {
2371 auto ts = gb.textsize;
2372 if (pos < 0) { pos = 0; return '\n'; }
2373 if (pos >= ts) { pos = ts; return '\n'; }
2374 if (!lc.utfuck) {
2375 immutable char ch = gb[pos++];
2376 final switch (codepage) {
2377 case CodePage.koi8u: return koi2uni(ch);
2378 case CodePage.cp1251: return cp12512uni(ch);
2379 case CodePage.cp866: return cp8662uni(ch);
2381 assert(0);
2383 Utf8DecoderFast udc;
2384 while (pos < ts) {
2385 if (udc.decodeSafe(cast(ubyte)gb[pos++])) return cast(dchar)udc.codepoint;
2387 return udc.replacement;
2390 /// this works correctly with utfuck
2391 int nextpos (int pos) const pure {
2392 if (pos < 0) return 0;
2393 immutable ts = gb.textsize;
2394 if (pos >= ts) return ts;
2395 if (!lc.utfuck) return pos+1;
2396 Utf8DecoderFast udc;
2397 while (pos < ts) if (udc.decodeSafe(cast(ubyte)gb[pos++])) break;
2398 return pos;
2401 /// this sometimes works correctly with utfuck
2402 int prevpos (int pos) const pure {
2403 if (pos <= 0) return 0;
2404 immutable ts = gb.textsize;
2405 if (ts == 0) return 0;
2406 if (pos > ts) pos = ts;
2407 --pos;
2408 if (lc.utfuck) {
2409 while (pos > 0 && !isValidUtf8Start(cast(ubyte)gb[pos])) --pos;
2411 return pos;
2414 bool textChanged () const pure { pragma(inline, true); return txchanged; } ///
2415 void textChanged (bool v) pure { pragma(inline, true); txchanged = v; } ///
2417 bool visualtabs () const pure { pragma(inline, true); return (lc.visualtabs && lc.tabsize > 0); } ///
2420 void visualtabs (bool v) {
2421 if (lc.visualtabs != v) {
2422 lc.visualtabs = v;
2423 fullDirty();
2427 ubyte tabsize () const pure { pragma(inline, true); return lc.tabsize; } ///
2430 void tabsize (ubyte v) {
2431 if (lc.tabsize != v) {
2432 lc.tabsize = v;
2433 if (lc.visualtabs) fullDirty();
2437 /// mark whole visible text as dirty
2438 final void fullDirty () nothrow { dirtyLines[] = -1; }
2442 @property void topline (int v) nothrow {
2443 if (v < 0) v = 0;
2444 if (v > lc.linecount) v = lc.linecount-1;
2445 immutable auto moldtop = mTopLine;
2446 mTopLine = v; // for linesPerWindow
2447 if (v+linesPerWindow > lc.linecount) {
2448 v = lc.linecount-linesPerWindow;
2449 if (v < 0) v = 0;
2451 if (v != moldtop) {
2452 mTopLine = moldtop;
2453 pushUndoCurPos();
2454 mTopLine = v;
2458 /// absolute coordinates in text
2459 final void gotoXY(bool vcenter=false) (int nx, int ny) nothrow {
2460 if (nx < 0) nx = 0;
2461 if (ny < 0) ny = 0;
2462 if (ny >= lc.linecount) ny = lc.linecount-1;
2463 auto pos = lc.xy2pos(nx, ny);
2464 lc.pos2xy(pos, nx, ny);
2465 if (nx != cx || ny != cy) {
2466 pushUndoCurPos();
2467 cx = nx;
2468 cy = ny;
2469 static if (vcenter) makeCurLineVisibleCentered(); else makeCurLineVisible();
2474 final void gotoPos(bool vcenter=false) (int pos) nothrow {
2475 if (pos < 0) pos = 0;
2476 if (pos > gb.textsize) pos = gb.textsize;
2477 int rx, ry;
2478 lc.pos2xy(pos, rx, ry);
2479 gotoXY!vcenter(rx, ry);
2482 final int curpos () nothrow { pragma(inline, true); return lc.xy2pos(cx, cy); } ///
2483 final void curpos (int pos) nothrow { pragma(inline, true); gotoPos(pos); } ///
2486 void clearUndo () nothrow {
2487 if (undo !is null) undo.clear();
2488 if (redo !is null) redo.clear();
2492 void clear () nothrow {
2493 lc.clear();
2494 txchanged = false;
2495 if (undo !is null) undo.clear();
2496 if (redo !is null) redo.clear();
2497 cx = cy = mTopLine = mXOfs = 0;
2498 prevTopLine = -1;
2499 prevXOfs = -1;
2500 dirtyLines[] = -1;
2501 bstart = bend = -1;
2502 markingBlock = false;
2503 lastBGEnd = false;
2504 txchanged = false;
2508 void clearAndDisableUndo () {
2509 if (undo !is null) delete undo;
2510 if (redo !is null) delete redo;
2514 void reinstantiateUndo () {
2515 if (undo is null) undo = new UndoStack(mAsRich, false, !mSingleLine);
2516 if (redo is null) redo = new UndoStack(mAsRich, true, !mSingleLine);
2520 void loadFile (const(char)[] fname) { loadFile(VFile(fname)); }
2523 void loadFile (VFile fl) {
2524 import core.stdc.stdlib : malloc, free;
2525 clear();
2526 scope(failure) clear();
2527 auto fpos = fl.tell;
2528 auto fsz = fl.size;
2529 if (fpos < fsz) {
2530 if (fsz-fpos >= gb.tbmax) throw new Exception("text too big");
2531 uint filesize = cast(uint)(fsz-fpos);
2532 if (!lc.resizeBuffer(filesize)) throw new Exception("text too big");
2533 scope(failure) clear();
2534 fl.rawReadExact(lc.getBufferPtr[]);
2535 if (!lc.rebuild()) throw new Exception("out of memory");
2540 void saveFile (const(char)[] fname) { saveFile(VFile(fname, "w")); }
2543 void saveFile (VFile fl) {
2544 gb.forEachBufPart(0, gb.textsize, delegate (const(char)[] buf) { fl.rawWriteExact(buf); });
2545 txchanged = false;
2546 if (undo !is null) undo.alwaysChanged();
2547 if (redo !is null) redo.alwaysChanged();
2550 /// attach new highlighter; return previous one
2551 /// note that you can't reuse one highlighter for several editors!
2552 EditorHL attachHiglighter (EditorHL ahl) {
2553 if (mAsRich) { assert(hl is null); return null; } // oops
2554 if (ahl is hl) return ahl; // nothing to do
2555 EditorHL prevhl = hl;
2556 if (ahl is null) {
2557 // detach
2558 if (hl !is null) {
2559 hl.gb = null;
2560 hl.lc = null;
2561 hl = null;
2562 gb.hasHiBuffer = false; // don't need it
2563 fullDirty();
2565 return prevhl; // return previous
2567 if (ahl.lc !is null) {
2568 if (ahl.lc !is lc) throw new Exception("highlighter already used by another editor");
2569 if (ahl !is hl) assert(0, "something is VERY wrong");
2570 return ahl;
2572 if (hl !is null) { hl.gb = null; hl.lc = null; }
2573 ahl.gb = gb;
2574 ahl.lc = lc;
2575 hl = ahl;
2576 gb.hasHiBuffer = true; // need it
2577 if (!gb.hasHiBuffer) assert(0, "out of memory"); // alas
2578 ahl.lineChanged(0, true);
2579 fullDirty();
2580 return prevhl;
2584 EditorHL detachHighlighter () {
2585 if (mAsRich) { assert(hl is null); return null; } // oops
2586 auto res = hl;
2587 if (res !is null) {
2588 hl.gb = null;
2589 hl.lc = null;
2590 hl = null;
2591 gb.hasHiBuffer = false; // don't need it
2592 fullDirty();
2594 return res;
2597 /// override this method to draw something before any other page drawing will be done
2598 public void drawPageBegin () {}
2600 /// override this method to draw one text line
2601 /// highlighting is done, other housekeeping is done, only draw
2602 /// lidx is always valid
2603 /// must repaint the whole line
2604 /// use `winXXX` vars to know window dimensions
2605 public abstract void drawLine (int lidx, int yofs, int xskip);
2607 /// just clear the line; you have to override this, 'cause it is used to clear empty space
2608 /// use `winXXX` vars to know window dimensions
2609 public abstract void drawEmptyLine (int yofs);
2611 /// override this method to draw something after page was drawn, but before drawing the status
2612 public void drawPageMisc () {}
2614 /// override this method to draw status line; it will be called after `drawPageBegin()`
2615 public void drawStatus () {}
2617 /// override this method to draw something after status was drawn, but before drawing the cursor
2618 public void drawPagePost () {}
2620 /// override this method to draw text cursor; it will be called after `drawPageMisc()`
2621 public abstract void drawCursor ();
2623 /// override this method to draw something (or flush drawing buffer) after everything was drawn
2624 public void drawPageEnd () {}
2626 /** draw the page; it will fix coords, call necessary methods and so on. you are usually don't need to override this.
2627 * page drawing flow:
2628 * drawPageBegin();
2629 * page itself with drawLine() or drawEmptyLine();
2630 * drawPageMisc();
2631 * drawStatus();
2632 * drawPagePost();
2633 * drawCursor();
2634 * drawPageEnd();
2636 void drawPage () {
2637 makeCurLineVisible();
2639 if (prevTopLine != mTopLine || prevXOfs != mXOfs) {
2640 prevTopLine = mTopLine;
2641 prevXOfs = mXOfs;
2642 dirtyLines[] = -1;
2645 drawPageBegin();
2646 immutable int lhp = lineHeightPixels;
2647 immutable int ydelta = (inPixels ? lhp : 1);
2648 bool alwaysDirty = false;
2649 auto pos = lc.xy2pos(0, mTopLine);
2650 auto lc = lc.linecount;
2651 int lyofs = 0;
2652 //TODO: optimize redrawing for variable line height mode
2653 foreach (int y; 0..visibleLinesPerWindow) {
2654 bool dirty = (mTopLine+y < lc && hl !is null && hl.fixLine(mTopLine+y));
2655 if (!alwaysDirty) {
2656 if (lhp < 0) {
2657 // variable line height, hacks
2658 alwaysDirty = (!alwaysDirty && y < dirtyLines.length ? (dirtyLines.ptr[y] != linePixelHeight(mTopLine+y)) : true);
2659 } else if (!dirty && y < dirtyLines.length) {
2660 // tty or constant pixel height
2661 dirty = (dirtyLines.ptr[y] != 0);
2663 dirty = true;
2665 if (dirty || alwaysDirty) {
2666 if (y < dirtyLines.length) dirtyLines.ptr[y] = (lhp >= 0 ? 0 : linePixelHeight(mTopLine+y));
2667 if (mTopLine+y < lc) {
2668 drawLine(mTopLine+y, lyofs, mXOfs);
2669 } else {
2670 drawEmptyLine(lyofs);
2673 lyofs += (ydelta > 0 ? ydelta : linePixelHeight(mTopLine+y));
2675 drawPageMisc();
2676 drawStatus();
2677 drawPagePost();
2678 drawCursor();
2679 drawPageEnd();
2682 /// force cursor coordinates to be in text
2683 final void normXY () nothrow {
2684 lc.pos2xy(curpos, cx, cy);
2688 final void makeCurXVisible () nothrow {
2689 // use "real" x coordinate to calculate x offset
2690 if (cx < 0) cx = 0;
2691 int rx;
2692 if (!inPixels) {
2693 int ry;
2694 lc.pos2xyVT(curpos, rx, ry);
2695 if (rx < mXOfs) mXOfs = rx;
2696 if (rx-mXOfs >= winw) mXOfs = rx-winw+1;
2697 } else {
2698 rx = localCursorX();
2699 rx += mXOfs;
2700 if (rx < mXOfs) mXOfs = rx-8;
2701 if (rx+4-mXOfs > winw) mXOfs = rx-winw+4;
2703 if (mXOfs < 0) mXOfs = 0;
2706 /// in symbols, not chars
2707 final int linelen (int lidx) nothrow {
2708 if (lidx < 0 || lidx >= lc.linecount) return 0;
2709 auto pos = lc.line2pos(lidx);
2710 auto ts = gb.textsize;
2711 if (pos > ts) pos = ts;
2712 int res = 0;
2713 if (!lc.utfuck) {
2714 if (mSingleLine) return ts-pos;
2715 while (pos < ts) {
2716 if (gb[pos++] == '\n') break;
2717 ++res;
2719 } else {
2720 immutable bool sl = mSingleLine;
2721 while (pos < ts) {
2722 char ch = gb[pos++];
2723 if (!sl && ch == '\n') break;
2724 ++res;
2725 if (ch >= 128) {
2726 --pos;
2727 pos += gb.utfuckLenAt(pos);
2731 return res;
2734 /// cursor position in "local" coords: from widget (x0,y0), possibly in pixels
2735 final int localCursorX () nothrow {
2736 int rx;
2737 localCursorXY(&rx, null);
2738 return rx;
2741 /// cursor position in "local" coords: from widget (x0,y0), possibly in pixels
2742 final void localCursorXY (int* lcx, int* lcy) nothrow {
2743 int rx, ry;
2744 if (!inPixels) {
2745 lc.pos2xyVT(curpos, rx, ry);
2746 ry -= mTopLine;
2747 rx -= mXOfs;
2748 if (lcx !is null) *lcx = rx;
2749 if (lcy !is null) *lcy = ry;
2750 } else {
2751 lc.pos2xy(curpos, rx, ry);
2752 if (textMeter is null) assert(0, "you forgot to setup `textMeter` for EditorEngine");
2753 if (lcy !is null) {
2754 if (lineHeightPixels > 0) {
2755 *lcy = (ry-mTopLine)*lineHeightPixels;
2756 } else {
2757 if (ry >= mTopLine) {
2758 for (int ll = mTopLine; ll < ry; ++ll) *lcy += lc.lineHeightPixels(ll);
2759 } else {
2760 for (int ll = mTopLine-1; ll >= ry; --ll) *lcy -= lc.lineHeightPixels(ll);
2764 if (rx == 0) { if (lcx !is null) *lcx = 0-mXOfs; return; }
2765 if (lcx !is null) {
2766 textMeter.reset(visualtabs ? lc.tabsize : 0);
2767 scope(exit) textMeter.finish(); // just in case
2768 auto pos = lc.linestart(ry);
2769 immutable int le = lc.linestart(ry+1);
2770 immutable bool ufuck = lc.utfuck;
2771 if (mSingleLine) {
2772 while (pos < le) {
2773 // advance one symbol
2774 textMeter.advance(dcharAtAdvance(pos), gb.hi(pos));
2775 --rx;
2776 if (rx == 0) break;
2778 } else {
2779 while (pos < le) {
2780 // advance one symbol
2781 if (gb[pos] == '\n') break;
2782 textMeter.advance(dcharAtAdvance(pos), gb.hi(pos));
2783 --rx;
2784 if (rx == 0) {
2785 // hack for kerning
2786 if (gb[pos] != '\n' && pos < le) {
2787 textMeter.advance(dcharAtAdvance(pos), gb.hi(pos));
2788 *lcx = textMeter.currofs;
2789 return;
2791 break;
2795 *lcx = textMeter.currwdt-mXOfs;
2800 /// convert coordinates in widget into text coordinates; can be used to convert mouse click position into text position
2801 /// WARNING: ty can be equal to linecount or -1!
2802 final void widget2text (int mx, int my, out int tx, out int ty) nothrow {
2803 if (!inPixels) {
2804 int ry = my+mTopLine;
2805 if (ry < 0) { ty = -1; return; } // tx is zero here
2806 if (ry >= lc.linecount) { ty = lc.linecount; return; } // tx is zero here
2807 if (mx <= 0 && mXOfs == 0) return; // tx is zero here
2808 // ah, screw it! user should not call this very often, so i can stop caring about speed.
2809 int visx = -mXOfs;
2810 auto pos = lc.line2pos(ry);
2811 auto ts = gb.textsize;
2812 int rx = 0;
2813 immutable bool ufuck = lc.utfuck;
2814 immutable bool sl = mSingleLine;
2815 while (pos < ts) {
2816 // advance one symbol
2817 char ch = gb[pos];
2818 if (!sl && ch == '\n') { tx = rx; return; } // done anyway
2819 int nextx = visx+1;
2820 if (ch == '\t' && visualtabs) {
2821 // hack!
2822 nextx = ((visx+mXOfs)/tabsize+1)*tabsize-mXOfs;
2824 if (mx >= visx && mx < nextx) { tx = rx; return; }
2825 visx = nextx;
2826 if (!ufuck || ch < 128) {
2827 ++pos;
2828 } else {
2829 pos = nextpos(pos);
2831 ++rx;
2833 } else {
2834 if (textMeter is null) assert(0, "you forgot to setup `textMeter` for EditorEngine");
2835 int ry;
2836 if (lineHeightPixels > 0) {
2837 ry = my/lineHeightPixels+mTopLine;
2838 } else {
2839 ry = mTopLine;
2840 if (my >= 0) {
2841 // down
2842 int lcy = 0;
2843 while (lcy < my) {
2844 lcy += lc.lineHeightPixels(ry);
2845 if (lcy > my) break;
2846 ++ry;
2847 if (lcy == my) break;
2849 } else {
2850 // up
2851 ry = mTopLine-1;
2852 int lcy = 0;
2853 while (ry >= 0) {
2854 int upy = lcy-lc.lineHeightPixels(ry);
2855 if (my >= upy && my < lcy) break;
2856 lcy = upy;
2860 if (ry < 0) { ty = -1; return; } // tx is zero here
2861 if (ry >= lc.linecount) { ty = lc.linecount; return; } // tx is zero here
2862 ty = ry;
2863 if (mx <= 0 && mXOfs == 0) return; // tx is zero here
2864 // now the hard part
2865 textMeter.reset(visualtabs ? lc.tabsize : 0);
2866 scope(exit) textMeter.finish(); // just in case
2867 int visx0 = -mXOfs;
2868 auto pos = lc.line2pos(ry);
2869 immutable ts = gb.textsize;
2870 int rx = 0;
2871 immutable bool ufuck = lc.utfuck;
2872 immutable bool sl = mSingleLine;
2873 while (pos < ts) {
2874 // advance one symbol
2875 char ch = gb[pos];
2876 if (!sl && ch == '\n') { tx = rx; return; } // done anyway
2877 if (!ufuck || ch < 128) {
2878 textMeter.advance(cast(dchar)ch, gb.hi(pos));
2879 ++pos;
2880 } else {
2881 textMeter.advance(dcharAtAdvance(pos), gb.hi(pos));
2883 immutable int visx1 = textMeter.currwdt-mXOfs;
2884 // visx0 is current char x start
2885 // visx1 is current char x end
2886 // so if our mx is in [visx0..visx1), we are at current char
2887 if (mx >= visx0 && mx < visx1) {
2888 // it is more natural this way
2889 if (mx >= visx0+(visx1-visx0)/2 && pos < lc.linestart(ry+1)) ++rx;
2890 tx = rx;
2891 return;
2893 ++rx;
2894 visx0 = visx1;
2900 final void makeCurLineVisible () nothrow {
2901 if (cy < 0) cy = 0;
2902 if (cy >= lc.linecount) cy = lc.linecount-1;
2903 if (cy < mTopLine) {
2904 mTopLine = cy;
2905 } else {
2906 if (cy > mTopLine+linesPerWindow-1) {
2907 mTopLine = cy-linesPerWindow+1;
2908 if (mTopLine < 0) mTopLine = 0;
2911 setDirtyLinesLength(visibleLinesPerWindow);
2912 makeCurXVisible();
2916 final void makeCurLineVisibleCentered (bool forced=false) nothrow {
2917 if (forced || !isCurLineVisible) {
2918 if (cy < 0) cy = 0;
2919 if (cy >= lc.linecount) cy = lc.linecount-1;
2920 mTopLine = cy-linesPerWindow/2;
2921 if (mTopLine < 0) mTopLine = 0;
2922 if (mTopLine+linesPerWindow > lc.linecount) {
2923 mTopLine = lc.linecount-linesPerWindow;
2924 if (mTopLine < 0) mTopLine = 0;
2927 setDirtyLinesLength(visibleLinesPerWindow);
2928 makeCurXVisible();
2932 final bool isCurLineBeforeTop () nothrow {
2933 pragma(inline, true);
2934 return (cy < mTopLine);
2938 final bool isCurLineAfterBottom () nothrow {
2939 pragma(inline, true);
2940 return (cy > mTopLine+linesPerWindow-1);
2944 final bool isCurLineVisible () nothrow {
2945 pragma(inline, true);
2946 return (cy >= mTopLine && cy < mTopLine+linesPerWindow);
2947 //if (cy < mTopLine) return false;
2948 //if (cy > mTopLine+linesPerWindow-1) return false;
2949 //return true;
2952 /// `updateDown`: update all the page (as new lines was inserted/removed)
2953 final void lineChanged (int lidx, bool updateDown) {
2954 if (lidx < 0 || lidx >= lc.linecount) return;
2955 if (hl !is null) hl.lineChanged(lidx, updateDown);
2956 if (lidx < mTopLine) { if (updateDown) dirtyLines[] = -1; return; }
2957 if (lidx >= mTopLine+linesPerWindow) return;
2958 immutable stl = lidx-mTopLine;
2959 assert(stl >= 0);
2960 if (stl < dirtyLines.length) {
2961 if (updateDown) {
2962 dirtyLines[stl..$] = -1;
2963 } else {
2964 dirtyLines.ptr[stl] = -1;
2970 final void lineChangedByPos (int pos, bool updateDown) { return lineChanged(lc.pos2line(pos), updateDown); }
2973 final void markLinesDirty (int lidx, int count) nothrow {
2974 if (prevTopLine != mTopLine || prevXOfs != mXOfs) return; // we will refresh the whole page anyway
2975 if (count < 1 || lidx >= lc.linecount) return;
2976 if (count > lc.linecount) count = lc.linecount;
2977 if (lidx >= mTopLine+linesPerWindow) return;
2978 int le = lidx+count;
2979 if (le <= mTopLine) { dirtyLines[] = -1; return; } // just in case
2980 if (lidx < mTopLine) { dirtyLines[] = -1; lidx = mTopLine; return; } // just in cale
2981 if (le > mTopLine+visibleLinesPerWindow) le = mTopLine+visibleLinesPerWindow;
2982 immutable stl = lidx-mTopLine;
2983 assert(stl >= 0);
2984 if (stl < dirtyLines.length) {
2985 auto el = le-mTopLine;
2986 if (el > dirtyLines.length) el = cast(int)dirtyLines.length;
2987 dirtyLines.ptr[stl..el] = -1;
2992 final void markLinesDirtySE (int lidxs, int lidxe) nothrow {
2993 if (lidxe < lidxs) { int tmp = lidxs; lidxs = lidxe; lidxe = tmp; }
2994 markLinesDirty(lidxs, lidxe-lidxs+1);
2998 final void markRangeDirty (int pos, int len) nothrow {
2999 if (prevTopLine != mTopLine || prevXOfs != mXOfs) return; // we will refresh the whole page anyway
3000 int l0 = lc.pos2line(pos);
3001 int l1 = lc.pos2line(pos+len+1);
3002 markLinesDirtySE(l0, l1);
3006 final void markBlockDirty () nothrow {
3007 //FIXME: optimize updating with block boundaries
3008 if (bstart >= bend) return;
3009 markRangeDirty(bstart, bend-bstart);
3012 /// do various fixups before text deletion
3013 /// cursor coords *may* be already changed
3014 /// will be called before text deletion by `deleteText` or `replaceText` APIs
3015 /// eolcount: number of eols in (to be) deleted block
3016 protected void willBeDeleted (int pos, int len, int eolcount) nothrow {
3017 //FIXME: optimize updating with block boundaries
3018 if (len < 1) return; // just in case
3019 assert(pos >= 0 && cast(long)pos+len <= gb.textsize);
3020 bookmarkDeletionFix(pos, len, eolcount);
3021 if (hasMarkedBlock) {
3022 if (pos+len <= bstart) {
3023 // move whole block up
3024 markBlockDirty();
3025 bstart -= len;
3026 bend -= len;
3027 markBlockDirty();
3028 lastBGEnd = false;
3029 } else if (pos <= bstart && pos+len >= bend) {
3030 // whole block will be deleted
3031 doBlockResetMark(false); // skip undo
3032 } else if (pos >= bstart && pos+len <= bend) {
3033 // deleting something inside block, move end
3034 markBlockDirty();
3035 bend -= len;
3036 if (bstart >= bend) {
3037 doBlockResetMark(false); // skip undo
3038 } else {
3039 markBlockDirty();
3040 lastBGEnd = true;
3042 } else if (pos >= bstart && pos < bend && pos+len > bend) {
3043 // chopping block end
3044 markBlockDirty();
3045 bend = pos;
3046 if (bstart >= bend) {
3047 doBlockResetMark(false); // skip undo
3048 } else {
3049 markBlockDirty();
3050 lastBGEnd = true;
3056 /// do various fixups after text deletion
3057 /// cursor coords *may* be already changed
3058 /// will be called after text deletion by `deleteText` or `replaceText` APIs
3059 /// eolcount: number of eols in deleted block
3060 /// pos and len: they were valid *before* deletion!
3061 protected void wasDeleted (int pos, int len, int eolcount) nothrow {
3064 /// do various fixups before text insertion
3065 /// cursor coords *may* be already changed
3066 /// will be called before text insertion by `insertText` or `replaceText` APIs
3067 /// eolcount: number of eols in (to be) inserted block
3068 protected void willBeInserted (int pos, int len, int eolcount) nothrow {
3071 /// do various fixups after text insertion
3072 /// cursor coords *may* be already changed
3073 /// will be called after text insertion by `insertText` or `replaceText` APIs
3074 /// eolcount: number of eols in inserted block
3075 protected void wasInserted (int pos, int len, int eolcount) nothrow {
3076 //FIXME: optimize updating with block boundaries
3077 if (len < 1) return;
3078 assert(pos >= 0 && cast(long)pos+len <= gb.textsize);
3079 bookmarkInsertionFix(pos, len, eolcount);
3080 if (markingBlock && pos == bend) {
3081 bend += len;
3082 markBlockDirty();
3083 lastBGEnd = true;
3084 return;
3086 if (hasMarkedBlock) {
3087 if (pos <= bstart) {
3088 // move whole block down
3089 markBlockDirty();
3090 bstart += len;
3091 bend += len;
3092 markBlockDirty();
3093 lastBGEnd = false;
3094 } else if (pos < bend) {
3095 // move end of block down
3096 markBlockDirty();
3097 bend += len;
3098 markBlockDirty();
3099 lastBGEnd = true;
3104 /// should be called after cursor position change
3105 protected final void growBlockMark () nothrow {
3106 if (!markingBlock || bstart < 0) return;
3107 makeCurLineVisible();
3108 int ry;
3109 int pos = curpos;
3110 if (pos < bstart) {
3111 if (lastBGEnd) {
3112 // move end
3113 ry = lc.pos2line(bend);
3114 bend = bstart;
3115 bstart = pos;
3116 lastBGEnd = false;
3117 } else {
3118 // move start
3119 ry = lc.pos2line(bstart);
3120 if (bstart == pos) return;
3121 bstart = pos;
3122 lastBGEnd = false;
3124 } else if (pos > bend) {
3125 // move end
3126 if (bend == pos) return;
3127 ry = lc.pos2line(bend-1);
3128 bend = pos;
3129 lastBGEnd = true;
3130 } else if (pos >= bstart && pos < bend) {
3131 // shrink block
3132 if (lastBGEnd) {
3133 // from end
3134 if (bend == pos) return;
3135 ry = lc.pos2line(bend-1);
3136 bend = pos;
3137 } else {
3138 // from start
3139 if (bstart == pos) return;
3140 ry = lc.pos2line(bstart);
3141 bstart = pos;
3144 markLinesDirtySE(ry, cy);
3147 /// all the following text operations will be grouped into one undo action
3148 bool undoGroupStart () {
3149 return (undo !is null ? undo.addGroupStart(this) : false);
3152 /// end undo action started with `undoGroupStart()`
3153 bool undoGroupEnd () {
3154 return (undo !is null ? undo.addGroupEnd(this) : false);
3157 /// build autoindent for the current line, put it into `indentText`
3158 /// `indentText` will include '\n'
3159 protected final void buildIndent (int pos) {
3160 if (indentText.length) { indentText.length = 0; indentText.assumeSafeAppend; }
3161 void putToIT (char ch) {
3162 auto optr = indentText.ptr;
3163 indentText ~= ch;
3164 if (optr !is indentText.ptr) {
3165 import core.memory : GC;
3166 if (indentText.ptr is GC.addrOf(indentText.ptr)) {
3167 GC.setAttr(indentText.ptr, GC.BlkAttr.NO_INTERIOR); // less false positives
3171 putToIT('\n');
3172 pos = lc.line2pos(lc.pos2line(pos));
3173 auto ts = gb.textsize;
3174 int curx = 0;
3175 while (pos < ts) {
3176 if (curx == cx) break;
3177 auto ch = gb[pos];
3178 if (ch == '\n') break;
3179 if (ch > ' ') break;
3180 putToIT(ch);
3181 ++pos;
3182 ++curx;
3186 /// delete text, save undo, mark updated lines
3187 /// return `false` if operation cannot be performed
3188 /// if caller wants to delete more text than buffer has, it is ok
3189 /// calls `dg` *after* undo saving, but before `willBeDeleted()`
3190 final bool deleteText(string movecursor="none") (int pos, int count, scope void delegate (int pos, int count) dg=null) {
3191 static assert(movecursor == "none" || movecursor == "start" || movecursor == "end");
3192 if (mReadOnly) return false;
3193 killTextOnChar = false;
3194 auto ts = gb.textsize;
3195 if (pos < 0 || pos >= ts || count < 0) return false;
3196 if (ts-pos < count) count = ts-pos;
3197 if (count > 0) {
3198 bool undoOk = false;
3199 if (undo !is null) undoOk = undo.addTextRemove(this, pos, count);
3200 if (dg !is null) dg(pos, count);
3201 int delEols = (!mSingleLine ? gb.countEolsInRange(pos, count) : 0);
3202 willBeDeleted(pos, count, delEols);
3203 //writeLogAction(pos, -count);
3204 // hack: if new linecount is different, there was '\n' in text
3205 auto olc = lc.linecount;
3206 if (!lc.remove(pos, count)) {
3207 if (undoOk) undo.popUndo(); // remove undo record
3208 return false;
3210 txchanged = true;
3211 static if (movecursor != "none") lc.pos2xy(pos, cx, cy);
3212 wasDeleted(pos, count, delEols);
3213 lineChangedByPos(pos, (lc.linecount != olc));
3214 } else {
3215 static if (movecursor != "none") {
3216 int rx, ry;
3217 lc.pos2xy(curpos, rx, ry);
3218 if (rx != cx || ry != cy) {
3219 if (pushUndoCurPos()) {
3220 cx = rx;
3221 cy = ry;
3222 markLinesDirty(cy, 1);
3227 return true;
3230 /// ugly name is intentional
3231 /// this replaces editor text, clears undo and sets `killTextOnChar` if necessary
3232 /// it also ignores "readonly" flag
3233 final bool setNewText (const(char)[] text, bool killOnChar=true) {
3234 auto oldro = mReadOnly;
3235 scope(exit) mReadOnly = oldro;
3236 mReadOnly = false;
3237 clear();
3238 auto res = insertText!"end"(0, text);
3239 clearUndo();
3240 if (mSingleLine) killTextOnChar = killOnChar;
3241 fullDirty();
3242 return res;
3245 /// insert text, save undo, mark updated lines
3246 /// return `false` if operation cannot be performed
3247 final bool insertText(string movecursor="none", bool doIndent=true) (int pos, const(char)[] str) {
3248 static assert(movecursor == "none" || movecursor == "start" || movecursor == "end");
3249 if (mReadOnly) return false;
3250 if (mKillTextOnChar) {
3251 killTextOnChar = false;
3252 if (gb.textsize > 0) {
3253 undoGroupStart();
3254 bstart = bend = -1;
3255 markingBlock = false;
3256 deleteText!"start"(0, gb.textsize);
3257 undoGroupEnd();
3260 auto ts = gb.textsize;
3261 if (pos < 0 || str.length >= int.max/3) return false;
3262 if (pos > ts) pos = ts;
3263 if (str.length > 0) {
3264 int nlc = (!mSingleLine ? GapBuffer.countEols(str) : 0);
3265 static if (doIndent) {
3266 if (nlc) {
3267 // want indenting and has at least one newline, hard case
3268 buildIndent(pos);
3269 if (indentText.length) {
3270 int toinsert = cast(int)str.length+nlc*(cast(int)indentText.length-1);
3271 bool undoOk = false;
3272 bool doRollback = false;
3273 // record undo
3274 if (undo !is null) undoOk = undo.addTextInsert(this, pos, toinsert);
3275 willBeInserted(pos, toinsert, nlc);
3276 auto spos = pos;
3277 auto ipos = pos;
3278 while (str.length > 0) {
3279 int elp = GapBuffer.findEol(str);
3280 if (elp < 0) elp = cast(int)str.length;
3281 if (elp > 0) {
3282 // insert text
3283 auto newpos = lc.put(ipos, str[0..elp]);
3284 if (newpos < 0) { doRollback = true; break; }
3285 ipos = newpos;
3286 str = str[elp..$];
3287 } else {
3288 // insert newline
3289 assert(str[0] == '\n');
3290 auto newpos = lc.put(ipos, indentText);
3291 if (newpos < 0) { doRollback = true; break; }
3292 ipos = newpos;
3293 str = str[1..$];
3296 if (doRollback) {
3297 // operation failed, rollback it
3298 if (ipos > spos) lc.remove(spos, ipos-spos); // remove inserted text
3299 if (undoOk) undo.popUndo(); // remove undo record
3300 return false;
3302 //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); }
3303 assert(ipos-spos == toinsert);
3304 static if (movecursor == "start") lc.pos2xy(spos, cx, cy);
3305 else static if (movecursor == "end") lc.pos2xy(ipos, cx, cy);
3306 txchanged = true;
3307 lineChangedByPos(spos, true);
3308 wasInserted(spos, toinsert, nlc);
3309 return true;
3313 // either we don't want indenting, or there are no eols in new text
3315 bool undoOk = false;
3316 // record undo
3317 if (undo !is null) undoOk = undo.addTextInsert(this, pos, cast(int)str.length);
3318 willBeInserted(pos, cast(int)str.length, nlc);
3319 // insert text
3320 auto newpos = lc.put(pos, str[]);
3321 if (newpos < 0) {
3322 // operation failed, rollback it
3323 if (undoOk) undo.popUndo(); // remove undo record
3324 return false;
3326 static if (movecursor == "start") lc.pos2xy(pos, cx, cy);
3327 else static if (movecursor == "end") lc.pos2xy(newpos, cx, cy);
3328 txchanged = true;
3329 lineChangedByPos(pos, (nlc > 0));
3330 wasInserted(pos, newpos-pos, nlc);
3333 return true;
3336 /// replace text at pos, save undo, mark updated lines
3337 /// return `false` if operation cannot be performed
3338 final bool replaceText(string movecursor="none", bool doIndent=false) (int pos, int count, const(char)[] str) {
3339 static assert(movecursor == "none" || movecursor == "start" || movecursor == "end");
3340 if (mReadOnly) return false;
3341 if (count < 0 || pos < 0) return false;
3342 if (mKillTextOnChar) {
3343 killTextOnChar = false;
3344 if (gb.textsize > 0) {
3345 undoGroupStart();
3346 bstart = bend = -1;
3347 markingBlock = false;
3348 deleteText!"start"(0, gb.textsize);
3349 undoGroupEnd();
3352 auto ts = gb.textsize;
3353 if (pos >= ts) pos = ts;
3354 if (count > ts-pos) count = ts-pos;
3355 bool needToRestoreBlock = (markingBlock || hasMarkedBlock);
3356 auto bs = bstart;
3357 auto be = bend;
3358 auto mb = markingBlock;
3359 undoGroupStart();
3360 scope(exit) undoGroupEnd();
3361 auto ocp = curpos;
3362 deleteText!movecursor(pos, count);
3363 static if (movecursor == "none") { bool cmoved = false; if (ocp > pos) { cmoved = true; ocp -= count; } }
3364 if (insertText!(movecursor, doIndent)(pos, str)) {
3365 static if (movecursor == "none") { if (cmoved) ocp += count; }
3366 if (needToRestoreBlock && !hasMarkedBlock) {
3367 // restore block if it was deleted
3368 bstart = bs;
3369 bend = be-count+cast(int)str.length;
3370 markingBlock = mb;
3371 if (bend < bstart) markingBlock = false;
3372 lastBGEnd = true;
3373 } else if (hasMarkedBlock && bs == pos && bstart > pos) {
3374 // consider the case when replaced text is inside the block,
3375 // and block is starting on the text
3376 bstart = pos;
3377 lastBGEnd = false; //???
3378 markBlockDirty();
3380 return true;
3382 return false;
3386 bool doBlockWrite (const(char)[] fname) {
3387 killTextOnChar = false;
3388 if (!hasMarkedBlock) return true;
3389 if (bend-bstart <= 0) return true;
3390 return doBlockWrite(VFile(fname, "w"));
3394 bool doBlockWrite (VFile fl) {
3395 import core.stdc.stdlib : malloc, free;
3396 killTextOnChar = false;
3397 if (!hasMarkedBlock) return true;
3398 gb.forEachBufPart(bstart, bend-bstart, delegate (const(char)[] buf) { fl.rawWriteExact(buf); });
3399 return true;
3403 bool doBlockRead (const(char)[] fname) { return doBlockRead(VFile(fname)); }
3406 bool doBlockRead (VFile fl) {
3407 //FIXME: optimize this!
3408 import core.stdc.stdlib : realloc, free;
3409 import core.stdc.string : memcpy;
3410 // read block data into temp buffer
3411 if (mReadOnly) return false;
3412 killTextOnChar = false;
3413 char* btext;
3414 scope(exit) if (btext !is null) free(btext);
3415 int blen = 0;
3416 char[1024] tb = void;
3417 for (;;) {
3418 auto rd = fl.rawRead(tb[]);
3419 if (rd.length == 0) break;
3420 if (blen+rd.length > int.max/2) return false;
3421 auto nb = cast(char*)realloc(btext, blen+rd.length);
3422 if (nb is null) return false;
3423 btext = nb;
3424 memcpy(btext+blen, rd.ptr, rd.length);
3425 blen += cast(int)rd.length;
3427 return insertText!("start", false)(curpos, btext[0..blen]); // no indent
3431 bool doBlockDelete () {
3432 if (mReadOnly) return false;
3433 if (!hasMarkedBlock) return true;
3434 return deleteText!"start"(bstart, bend-bstart, (pos, count) { doBlockResetMark(false); });
3438 bool doBlockCopy () {
3439 //FIXME: optimize this!
3440 import core.stdc.stdlib : malloc, free;
3441 if (mReadOnly) return false;
3442 killTextOnChar = false;
3443 if (!hasMarkedBlock) return true;
3444 // copy block data into temp buffer
3445 int blen = bend-bstart;
3446 GapBuffer.HighState* hsbuf;
3447 scope(exit) if (hsbuf !is null) free(hsbuf);
3448 if (asRich) {
3449 // rich text: get atts
3450 hsbuf = cast(GapBuffer.HighState*)malloc(blen*hsbuf[0].sizeof);
3451 if (hsbuf is null) return false;
3452 foreach (int pp; bstart..bend) hsbuf[pp-bstart] = gb.hi(pp);
3454 // normal text
3455 char* btext = cast(char*)malloc(blen);
3456 if (btext is null) return false; // alas
3457 scope(exit) free(btext);
3458 foreach (int pp; bstart..bend) btext[pp-bstart] = gb[pp];
3459 auto stp = curpos;
3460 return insertText!("start", false)(stp, btext[0..blen]); // no indent
3461 // attrs
3462 if (asRich) {
3463 foreach (immutable int idx; 0..blen) gb.hi(stp+idx) = hsbuf[idx];
3468 bool doBlockMove () {
3469 //FIXME: optimize this!
3470 import core.stdc.stdlib : malloc, free;
3471 if (mReadOnly) return false;
3472 killTextOnChar = false;
3473 if (!hasMarkedBlock) return true;
3474 int pos = curpos;
3475 if (pos >= bstart && pos < bend) return false; // can't do this while we are inside the block
3476 // copy block data into temp buffer
3477 int blen = bend-bstart;
3478 GapBuffer.HighState* hsbuf;
3479 scope(exit) if (hsbuf !is null) free(hsbuf);
3480 if (asRich) {
3481 // rich text: get atts
3482 hsbuf = cast(GapBuffer.HighState*)malloc(blen*hsbuf[0].sizeof);
3483 if (hsbuf is null) return false;
3484 foreach (int pp; bstart..bend) hsbuf[pp-bstart] = gb.hi(pp);
3486 char* btext = cast(char*)malloc(blen);
3487 if (btext is null) return false; // alas
3488 scope(exit) free(btext);
3489 foreach (int pp; bstart..bend) btext[pp-bstart] = gb[pp];
3490 // group undo action
3491 bool undoOk = undoGroupStart();
3492 if (pos >= bstart) pos -= blen;
3493 if (!doBlockDelete()) {
3494 // rollback
3495 if (undoOk) undo.popUndo();
3496 return false;
3498 auto stp = pos;
3499 if (!insertText!("start", false)(pos, btext[0..blen])) {
3500 // rollback
3501 if (undoOk) undo.popUndo();
3502 return false;
3504 // attrs
3505 if (asRich) {
3506 foreach (immutable int idx; 0..blen) gb.hi(stp+idx) = hsbuf[idx];
3508 // mark moved block
3509 bstart = pos;
3510 bend = pos+blen;
3511 markBlockDirty();
3512 undoGroupEnd();
3513 return true;
3517 void doDelete () {
3518 if (mReadOnly) return;
3519 int pos = curpos;
3520 if (pos >= gb.textsize) return;
3521 if (!lc.utfuck) {
3522 deleteText!"start"(pos, 1);
3523 } else {
3524 deleteText!"start"(pos, gb.utfuckLenAt(pos));
3529 void doBackspace () {
3530 if (mReadOnly) return;
3531 killTextOnChar = false;
3532 int pos = curpos;
3533 if (pos == 0) return;
3534 immutable int ppos = prevpos(pos);
3535 deleteText!"start"(ppos, pos-ppos);
3539 void doBackByIndent () {
3540 if (mReadOnly) return;
3541 int pos = curpos;
3542 int ls = lc.xy2pos(0, cy);
3543 if (pos == ls) { doDeleteWord(); return; }
3544 if (gb[pos-1] > ' ') { doDeleteWord(); return; }
3545 int rx, ry;
3546 lc.pos2xy(pos, rx, ry);
3547 int del = 2-rx%2;
3548 if (del > 1 && (pos-2 < ls || gb[pos-2] > ' ')) del = 1;
3549 pos -= del;
3550 deleteText!"start"(pos, del);
3554 void doDeleteWord () {
3555 if (mReadOnly) return;
3556 int pos = curpos;
3557 if (pos == 0) return;
3558 auto ch = gb[pos-1];
3559 if (!mSingleLine && ch == '\n') { doBackspace(); return; }
3560 int stpos = pos-1;
3561 // find word start
3562 if (ch <= ' ') {
3563 while (stpos > 0) {
3564 ch = gb[stpos-1];
3565 if ((!mSingleLine && ch == '\n') || ch > ' ') break;
3566 --stpos;
3568 } else if (isWordChar(ch)) {
3569 while (stpos > 0) {
3570 ch = gb[stpos-1];
3571 if (!isWordChar(ch)) break;
3572 --stpos;
3575 if (pos == stpos) return;
3576 deleteText!"start"(stpos, pos-stpos);
3580 void doKillLine () {
3581 if (mReadOnly) return;
3582 int ls = lc.xy2pos(0, cy);
3583 int le;
3584 if (cy == lc.linecount-1) {
3585 le = gb.textsize;
3586 } else {
3587 le = lc.xy2pos(0, cy+1);
3589 if (ls < le) deleteText!"start"(ls, le-ls);
3593 void doKillToEOL () {
3594 if (mReadOnly) return;
3595 int pos = curpos;
3596 auto ts = gb.textsize;
3597 if (mSingleLine) {
3598 if (pos < ts) deleteText!"start"(pos, ts-pos);
3599 } else {
3600 if (pos < ts && gb[pos] != '\n') {
3601 int epos = pos+1;
3602 while (epos < ts && gb[epos] != '\n') ++epos;
3603 deleteText!"start"(pos, epos-pos);
3608 /// split line at current position
3609 bool doLineSplit (bool autoindent=true) {
3610 if (mReadOnly || mSingleLine) return false;
3611 if (autoindent) {
3612 return insertText!("end", true)(curpos, "\n");
3613 } else {
3614 return insertText!("end", false)(curpos, "\n");
3618 /// put char in koi8
3619 void doPutChar (char ch) {
3620 if (mReadOnly) return;
3621 if (!mSingleLine && ch == '\n') { doLineSplit(inPasteMode <= 0); return; }
3622 if (ch > 127 && lc.utfuck) {
3623 char[8] ubuf = void;
3624 int len = utf8Encode(ubuf[], koi2uni(ch));
3625 if (len < 1) { ubuf[0] = '?'; len = 1; }
3626 insertText!("end", true)(curpos, ubuf[0..len]);
3627 return;
3629 if (ch >= 128 && codepage != CodePage.koi8u) ch = recodeCharTo(ch);
3630 if (inPasteMode <= 0) {
3631 insertText!("end", true)(curpos, (&ch)[0..1]);
3632 } else {
3633 insertText!("end", false)(curpos, (&ch)[0..1]);
3638 void doPutDChar (dchar dch) {
3639 if (mReadOnly) return;
3640 if (!Utf8DecoderFast.isValidDC(dch)) dch = Utf8DecoderFast.replacement;
3641 if (dch < 128) { doPutChar(cast(char)dch); return; }
3642 char[4] ubuf = void;
3643 auto len = utf8Encode(ubuf[], dch);
3644 if (len < 1) return;
3645 if (lc.utfuck) {
3646 insertText!"end"(curpos, ubuf.ptr[0..len]);
3647 } else {
3648 // recode to codepage
3649 doPutChar(recodeU2B(dch));
3654 void doPutTextUtf (const(char)[] str) {
3655 if (mReadOnly) return;
3656 if (str.length == 0) return;
3658 if (!mSingleLine && utfuck && inPasteMode > 0) {
3659 insertText!"end"(curpos, str);
3660 return;
3663 bool ugstarted = false;
3664 void startug () { if (!ugstarted) { ugstarted = true; undoGroupStart(); } }
3665 scope(exit) if (ugstarted) undoGroupEnd();
3667 Utf8DecoderFast udc;
3668 foreach (immutable char ch; str) {
3669 if (udc.decodeSafe(cast(ubyte)ch)) {
3670 dchar dch = cast(dchar)udc.codepoint;
3671 if (!mSingleLine && dch == '\n') { startug(); doLineSplit(inPasteMode <= 0); continue; }
3672 startug();
3673 doPutChar(recodeU2B(dch));
3678 /// put text in koi8
3679 void doPutText (const(char)[] str) {
3680 if (mReadOnly) return;
3681 if (str.length == 0) return;
3683 if (!mSingleLine && !utfuck && codepage == CodePage.koi8u && inPasteMode > 0) {
3684 insertText!"end"(curpos, str);
3685 return;
3688 bool ugstarted = false;
3689 void startug () { if (!ugstarted) { ugstarted = true; undoGroupStart(); } }
3690 scope(exit) if (ugstarted) undoGroupEnd();
3692 usize pos = 0;
3693 char ch;
3694 while (pos < str.length) {
3695 auto stpos = pos;
3696 while (pos < str.length) {
3697 ch = str.ptr[pos];
3698 if (!mSingleLine && ch == '\n') break;
3699 if (ch >= 128) break;
3700 ++pos;
3702 if (stpos < pos) { startug(); insertText!"end"(curpos, str.ptr[stpos..pos]); }
3703 if (pos >= str.length) break;
3704 ch = str.ptr[pos];
3705 if (!mSingleLine && ch == '\n') { startug(); doLineSplit(inPasteMode <= 0); ++pos; continue; }
3706 if (ch < ' ') { startug(); insertText!"end"(curpos, str.ptr[pos..pos+1]); ++pos; continue; }
3707 Utf8DecoderFast udc;
3708 stpos = pos;
3709 while (pos < str.length) if (udc.decode(cast(ubyte)(str.ptr[pos++]))) break;
3710 startug();
3711 if (udc.complete) {
3712 insertText!"end"(curpos, str.ptr[stpos..pos]);
3713 } else {
3714 ch = uni2koi(Utf8DecoderFast.replacement);
3715 insertText!"end"(curpos, (&ch)[0..1]);
3721 void doPasteStart () {
3722 if (mKillTextOnChar) {
3723 killTextOnChar = false;
3724 if (gb.textsize > 0) {
3725 undoGroupStart();
3726 bstart = bend = -1;
3727 markingBlock = false;
3728 deleteText!"start"(0, gb.textsize);
3729 undoGroupEnd();
3732 undoGroupStart();
3733 ++inPasteMode;
3737 void doPasteEnd () {
3738 killTextOnChar = false;
3739 if (--inPasteMode < 0) inPasteMode = 0;
3740 undoGroupEnd();
3743 protected final bool xIndentLine (int lidx) {
3744 //TODO: rollback
3745 if (mReadOnly) return false;
3746 if (lidx < 0 || lidx >= lc.linecount) return false;
3747 auto pos = lc.xy2pos(0, lidx);
3748 auto epos = lc.xy2pos(0, lidx+1);
3749 auto stpos = pos;
3750 // if line consists of blanks only, don't do anything
3751 while (pos < epos) {
3752 auto ch = gb[pos];
3753 if (ch == '\n') return true;
3754 if (ch > ' ') break;
3755 ++pos;
3757 if (pos >= gb.textsize) return true;
3758 pos = stpos;
3759 char[2] spc = ' ';
3760 return insertText!("none", false)(pos, spc[]);
3764 void doIndentBlock () {
3765 if (mReadOnly) return;
3766 killTextOnChar = false;
3767 if (!hasMarkedBlock) return;
3768 int sy = lc.pos2line(bstart);
3769 int ey = lc.pos2line(bend-1);
3770 bool bsAtBOL = (bstart == lc.line2pos(sy));
3771 undoGroupStart();
3772 scope(exit) undoGroupEnd();
3773 foreach (int lidx; sy..ey+1) xIndentLine(lidx);
3774 if (bsAtBOL) bstart = lc.line2pos(sy); // line already marked as dirty
3777 protected final bool xUnindentLine (int lidx) {
3778 if (mReadOnly) return false;
3779 if (lidx < 0 || lidx >= lc.linecount) return true;
3780 auto pos = lc.xy2pos(0, lidx);
3781 auto len = 1;
3782 if (gb[pos] > ' ' || gb[pos] == '\n') return true;
3783 if (pos+1 < gb.textsize && gb[pos+1] <= ' ' && gb[pos+1] != '\n') ++len;
3784 return deleteText!"none"(pos, len);
3788 void doUnindentBlock () {
3789 if (mReadOnly) return;
3790 killTextOnChar = false;
3791 if (!hasMarkedBlock) return;
3792 int sy = lc.pos2line(bstart);
3793 int ey = lc.pos2line(bend-1);
3794 undoGroupStart();
3795 scope(exit) undoGroupEnd();
3796 foreach (int lidx; sy..ey+1) xUnindentLine(lidx);
3799 // ////////////////////////////////////////////////////////////////////// //
3800 // actions
3802 /// push cursor position to undo stach
3803 final bool pushUndoCurPos () nothrow {
3804 return (undo !is null ? undo.addCurMove(this) : false);
3807 // returns old state
3808 enum SetupShiftMarkingMixin = q{
3809 auto omb = markingBlock;
3810 scope(exit) markingBlock = omb;
3811 if (domark) {
3812 if (!hasMarkedBlock) {
3813 int pos = curpos;
3814 bstart = bend = pos;
3815 lastBGEnd = true;
3817 markingBlock = true;
3822 void doWordLeft (bool domark=false) {
3823 mixin(SetupShiftMarkingMixin);
3824 killTextOnChar = false;
3825 int pos = curpos;
3826 if (pos == 0) return;
3827 auto ch = gb[pos-1];
3828 if (!mSingleLine && ch == '\n') { doLeft(); return; }
3829 int stpos = pos-1;
3830 // find word start
3831 if (ch <= ' ') {
3832 while (stpos > 0) {
3833 ch = gb[stpos-1];
3834 if ((!mSingleLine && ch == '\n') || ch > ' ') break;
3835 --stpos;
3838 if (stpos > 0 && isWordChar(ch)) {
3839 while (stpos > 0) {
3840 ch = gb[stpos-1];
3841 if (!isWordChar(ch)) break;
3842 --stpos;
3845 pushUndoCurPos();
3846 lc.pos2xy(stpos, cx, cy);
3847 growBlockMark();
3851 void doWordRight (bool domark=false) {
3852 mixin(SetupShiftMarkingMixin);
3853 killTextOnChar = false;
3854 int pos = curpos;
3855 if (pos == gb.textsize) return;
3856 auto ch = gb[pos];
3857 if (!mSingleLine && ch == '\n') { doRight(); return; }
3858 int epos = pos+1;
3859 // find word start
3860 if (ch <= ' ') {
3861 while (epos < gb.textsize) {
3862 ch = gb[epos];
3863 if ((!mSingleLine && ch == '\n') || ch > ' ') break;
3864 ++epos;
3866 } else if (isWordChar(ch)) {
3867 while (epos < gb.textsize) {
3868 ch = gb[epos];
3869 if (!isWordChar(ch)) {
3870 if (ch <= ' ') {
3871 while (epos < gb.textsize) {
3872 ch = gb[epos];
3873 if ((!mSingleLine && ch == '\n') || ch > ' ') break;
3874 ++epos;
3877 break;
3879 ++epos;
3882 pushUndoCurPos();
3883 lc.pos2xy(epos, cx, cy);
3884 growBlockMark();
3888 void doTextTop (bool domark=false) {
3889 mixin(SetupShiftMarkingMixin);
3890 if (lc.mLineCount < 2) return;
3891 killTextOnChar = false;
3892 if (mTopLine == 0 && cy == 0) return;
3893 pushUndoCurPos();
3894 mTopLine = cy = 0;
3895 growBlockMark();
3899 void doTextBottom (bool domark=false) {
3900 mixin(SetupShiftMarkingMixin);
3901 if (lc.mLineCount < 2) return;
3902 killTextOnChar = false;
3903 if (cy >= lc.linecount-1) return;
3904 pushUndoCurPos();
3905 cy = lc.linecount-1;
3906 growBlockMark();
3910 void doPageTop (bool domark=false) {
3911 mixin(SetupShiftMarkingMixin);
3912 if (lc.mLineCount < 2) return;
3913 killTextOnChar = false;
3914 if (cy == mTopLine) return;
3915 pushUndoCurPos();
3916 cy = mTopLine;
3917 growBlockMark();
3921 void doPageBottom (bool domark=false) {
3922 mixin(SetupShiftMarkingMixin);
3923 if (lc.mLineCount < 2) return;
3924 killTextOnChar = false;
3925 int ny = mTopLine+linesPerWindow-1;
3926 if (ny >= lc.linecount) ny = lc.linecount-1;
3927 if (cy != ny) {
3928 pushUndoCurPos();
3929 cy = ny;
3931 growBlockMark();
3935 void doScrollUp (bool domark=false) {
3936 mixin(SetupShiftMarkingMixin);
3937 if (mTopLine > 0) {
3938 killTextOnChar = false;
3939 pushUndoCurPos();
3940 --mTopLine;
3941 --cy;
3942 } else if (cy > 0) {
3943 killTextOnChar = false;
3944 pushUndoCurPos();
3945 --cy;
3947 growBlockMark();
3951 void doScrollDown (bool domark=false) {
3952 mixin(SetupShiftMarkingMixin);
3953 if (mTopLine+linesPerWindow < lc.linecount) {
3954 killTextOnChar = false;
3955 pushUndoCurPos();
3956 ++mTopLine;
3957 ++cy;
3958 } else if (cy < lc.linecount-1) {
3959 killTextOnChar = false;
3960 pushUndoCurPos();
3961 ++cy;
3963 growBlockMark();
3967 void doUp (bool domark=false) {
3968 mixin(SetupShiftMarkingMixin);
3969 if (cy > 0) {
3970 killTextOnChar = false;
3971 pushUndoCurPos();
3972 --cy;
3973 // visjump
3974 if (winh >= 24 && isCurLineBeforeTop) {
3975 mTopLine = cy-(winh/3);
3976 if (mTopLine < 0) mTopLine = 0;
3977 setDirtyLinesLength(visibleLinesPerWindow);
3978 makeCurXVisible();
3981 growBlockMark();
3985 void doDown (bool domark=false) {
3986 mixin(SetupShiftMarkingMixin);
3987 if (cy < lc.linecount-1) {
3988 killTextOnChar = false;
3989 pushUndoCurPos();
3990 ++cy;
3991 // visjump
3992 if (winh >= 24 && isCurLineAfterBottom) {
3993 mTopLine = cy+(winh/3)-linesPerWindow+1;
3994 if (mTopLine < 0) mTopLine = 0;
3995 setDirtyLinesLength(visibleLinesPerWindow);
3996 makeCurXVisible();
3999 growBlockMark();
4003 void doLeft (bool domark=false) {
4004 mixin(SetupShiftMarkingMixin);
4005 int rx, ry;
4006 killTextOnChar = false;
4007 lc.pos2xy(curpos, rx, ry);
4008 if (cx > rx) cx = rx;
4009 if (cx > 0) {
4010 pushUndoCurPos();
4011 --cx;
4012 } else if (cy > 0) {
4013 // to prev line
4014 pushUndoCurPos();
4015 lc.pos2xy(lc.xy2pos(0, cy)-1, cx, cy);
4017 growBlockMark();
4021 void doRight (bool domark=false) {
4022 mixin(SetupShiftMarkingMixin);
4023 int rx, ry;
4024 killTextOnChar = false;
4025 lc.pos2xy(lc.xy2pos(cx+1, cy), rx, ry);
4026 if (cx+1 > rx) {
4027 if (cy < lc.linecount-1) {
4028 pushUndoCurPos();
4029 cx = 0;
4030 ++cy;
4032 } else {
4033 pushUndoCurPos();
4034 ++cx;
4036 growBlockMark();
4040 void doPageUp (bool domark=false) {
4041 mixin(SetupShiftMarkingMixin);
4042 if (linesPerWindow < 2 || lc.mLineCount < 2) return;
4043 killTextOnChar = false;
4044 int ntl = mTopLine-(linesPerWindow-1);
4045 int ncy = cy-(linesPerWindow-1);
4046 if (ntl < 0) ntl = 0;
4047 if (ncy < 0) ncy = 0;
4048 if (ntl != mTopLine || ncy != cy) {
4049 pushUndoCurPos();
4050 mTopLine = ntl;
4051 cy = ncy;
4053 growBlockMark();
4057 void doPageDown (bool domark=false) {
4058 mixin(SetupShiftMarkingMixin);
4059 if (linesPerWindow < 2 || lc.mLineCount < 2) return;
4060 killTextOnChar = false;
4061 int ntl = mTopLine+(linesPerWindow-1);
4062 int ncy = cy+(linesPerWindow-1);
4063 if (ntl+linesPerWindow >= lc.linecount) ntl = lc.linecount-linesPerWindow;
4064 if (ncy >= lc.linecount) ncy = lc.linecount-1;
4065 if (ntl < 0) ntl = 0;
4066 if (ntl != mTopLine || ncy != cy) {
4067 pushUndoCurPos();
4068 mTopLine = ntl;
4069 cy = ncy;
4071 growBlockMark();
4075 void doHome (bool smart=true, bool domark=false) {
4076 mixin(SetupShiftMarkingMixin);
4077 killTextOnChar = false;
4078 if (cx != 0) {
4079 pushUndoCurPos();
4080 cx = 0;
4081 } else {
4082 if (!smart) return;
4083 int nx = 0;
4084 auto pos = lc.xy2pos(0, cy);
4085 while (pos < gb.textsize) {
4086 auto ch = gb[pos];
4087 if (!mSingleLine && ch == '\n') return;
4088 if (ch > ' ') break;
4089 ++pos;
4090 ++nx;
4092 if (nx != cx) {
4093 pushUndoCurPos();
4094 cx = nx;
4097 growBlockMark();
4101 void doEnd (bool domark=false) {
4102 mixin(SetupShiftMarkingMixin);
4103 int rx, ry;
4104 killTextOnChar = false;
4105 int ep;
4106 if (cy >= lc.linecount-1) {
4107 ep = gb.textsize;
4108 } else {
4109 ep = lc.lineend(cy);
4110 //if (gb[ep] != '\n') ++ep; // word wrapping
4112 lc.pos2xy(ep, rx, ry);
4113 if (rx != cx || ry != cy) {
4114 pushUndoCurPos();
4115 cx = rx;
4116 cy = ry;
4118 growBlockMark();
4122 /*private*/protected void doUndoRedo (UndoStack us) { // "allMembers" trait: shut the fuck up!
4123 if (us is null) return;
4124 killTextOnChar = false;
4125 int level = 0;
4126 while (us.hasUndo) {
4127 auto tp = us.undoAction(this);
4128 switch (tp) {
4129 case UndoStack.Type.GroupStart:
4130 if (--level <= 0) return;
4131 break;
4132 case UndoStack.Type.GroupEnd:
4133 ++level;
4134 break;
4135 default:
4136 if (level <= 0) return;
4137 break;
4142 void doUndo () { doUndoRedo(undo); } ///
4143 void doRedo () { doUndoRedo(redo); } ///
4146 void doBlockResetMark (bool saveUndo=true) nothrow {
4147 killTextOnChar = false;
4148 if (bstart < bend) {
4149 if (saveUndo) pushUndoCurPos();
4150 markLinesDirtySE(lc.pos2line(bstart), lc.pos2line(bend-1));
4152 bstart = bend = -1;
4153 markingBlock = false;
4156 /// toggle block marking mode
4157 void doToggleBlockMarkMode () {
4158 killTextOnChar = false;
4159 if (bstart == bend && markingBlock) { doBlockResetMark(false); return; }
4160 if (bstart < bend && !markingBlock) doBlockResetMark(false);
4161 int pos = curpos;
4162 if (!hasMarkedBlock) {
4163 bstart = bend = pos;
4164 markingBlock = true;
4165 lastBGEnd = true;
4166 } else {
4167 if (pos != bstart) {
4168 bend = pos;
4169 if (bend < bstart) { pos = bstart; bstart = bend; bend = pos; }
4171 markingBlock = false;
4172 dirtyLines[] = -1; //FIXME: optimize
4177 void doSetBlockStart () {
4178 killTextOnChar = false;
4179 auto pos = curpos;
4180 if ((hasMarkedBlock || (bstart == bend && bstart >= 0 && bstart < gb.textsize)) && pos < bend) {
4181 //if (pos < bstart) markRangeDirty(pos, bstart-pos); else markRangeDirty(bstart, pos-bstart);
4182 bstart = pos;
4183 lastBGEnd = false;
4184 } else {
4185 doBlockResetMark();
4186 bstart = bend = pos;
4187 lastBGEnd = false;
4189 markingBlock = false;
4190 dirtyLines[] = -1; //FIXME: optimize
4194 void doSetBlockEnd () {
4195 auto pos = curpos;
4196 if ((hasMarkedBlock || (bstart == bend && bstart >= 0 && bstart < gb.textsize)) && pos > bstart) {
4197 //if (pos < bend) markRangeDirty(pos, bend-pos); else markRangeDirty(bend, pos-bend);
4198 bend = pos;
4199 lastBGEnd = true;
4200 } else {
4201 doBlockResetMark();
4202 bstart = bend = pos;
4203 lastBGEnd = true;
4205 markingBlock = false;
4206 dirtyLines[] = -1; //FIXME: optimize
4209 protected:
4210 // called by undo/redo processors
4211 final void ubTextRemove (int pos, int len) {
4212 if (mReadOnly) return;
4213 killTextOnChar = false;
4214 int nlc = (!mSingleLine ? gb.countEolsInRange(pos, len) : 0);
4215 bookmarkDeletionFix(pos, len, nlc);
4216 lineChangedByPos(pos, (nlc > 0));
4217 lc.remove(pos, len);
4220 // called by undo/redo processors
4221 final bool ubTextInsert (int pos, const(char)[] str) {
4222 if (mReadOnly) return true;
4223 killTextOnChar = false;
4224 if (str.length == 0) return true;
4225 int nlc = (!mSingleLine ? gb.countEols(str) : 0);
4226 bookmarkInsertionFix(pos, pos+cast(int)str.length, nlc);
4227 if (lc.put(pos, str) >= 0) {
4228 lineChangedByPos(pos, (nlc > 0));
4229 return true;
4230 } else {
4231 return false;
4235 // can be called only after `ubTextInsert`, and with the same pos/length
4236 // usually it is done by undo/redo action if the editor is in "rich mode"
4237 final void ubTextSetAttrs (int pos, const(GapBuffer.HighState)[] hs) {
4238 if (mReadOnly || hs.length == 0) return;
4239 assert(gb.hasHiBuffer);
4240 foreach (const ref hi; hs) gb.hbuf[gb.pos2real(pos++)] = hi;
4243 // ////////////////////////////////////////////////////////////////////// //
4244 public static struct TextRange {
4245 private:
4246 EditorEngine ed;
4247 int pos;
4248 int left; // chars left, including front
4249 char frontch = 0;
4250 nothrow:
4251 private:
4252 this (EditorEngine aed, int apos, int aleft, char afrontch) pure {
4253 ed = aed;
4254 pos = apos;
4255 left = aleft;
4256 frontch = afrontch;
4259 this (EditorEngine aed, usize lo, usize hi) {
4260 ed = aed;
4261 if (aed !is null && lo < hi && lo < aed.gb.textsize) {
4262 pos = cast(int)lo;
4263 if (hi > ed.gb.textsize) hi = ed.gb.textsize;
4264 left = cast(int)hi-pos+1; // compensate for first popFront
4265 popFront();
4268 public:
4269 @property bool empty () const pure @safe @nogc { pragma(inline, true); return (left <= 0); }
4270 @property char front () const pure @safe @nogc { pragma(inline, true); return frontch; }
4271 void popFront () {
4272 if (ed is null || left < 2) { left = 0; frontch = 0; return; }
4273 --left;
4274 if (pos >= ed.gb.textsize) { left = 0; frontch = 0; return; }
4275 frontch = ed.gb[pos++];
4277 auto save () pure { pragma(inline, true); return TextRange(ed, pos, left, frontch); }
4278 @property usize length () const pure @safe @nogc { pragma(inline, true); return (left > 0 ? left : 0); }
4279 alias opDollar = length;
4280 char opIndex (usize idx) {
4281 pragma(inline, true);
4282 return (left > 0 && idx < left ? (idx == 0 ? frontch : ed.gb[pos+cast(int)idx-1]) : 0);
4284 auto opSlice () pure { pragma(inline, true); return this.save; }
4285 //WARNING: logic untested!
4286 auto opSlice (uint lo, uint hi) {
4287 if (ed is null || left <= 0 || lo >= left || lo >= hi) return TextRange(null, 0, 0);
4288 hi -= lo; // convert to length
4289 if (hi > left) hi = left;
4290 if (left-lo > hi) hi = left-lo;
4291 return TextRange(ed, cast(int)lo+1, cast(int)hi, ed.gb[cast(int)lo]);
4293 // make it bidirectional, just for fun
4294 //WARNING: completely untested!
4295 char back () const pure {
4296 pragma(inline, true);
4297 return (ed !is null && left > 0 ? (left == 1 ? frontch : ed.gb[pos+left-2]) : 0);
4299 void popBack () {
4300 if (ed is null || left < 2) { left = 0; frontch = 0; return; }
4301 --left;
4305 public:
4306 /// range interface to editor text
4307 /// WARNING! do not change anything while range is active, or results *WILL* be UD
4308 final TextRange opSlice (usize lo, usize hi) nothrow { return TextRange(this, lo, hi); }
4309 final TextRange opSlice () nothrow { return TextRange(this, 0, gb.textsize); } /// ditto
4310 final int opDollar () nothrow { return gb.textsize; } ///
4313 final TextRange markedBlockRange () nothrow {
4314 if (!hasMarkedBlock) return TextRange.init;
4315 return TextRange(this, bstart, bend);
4318 static:
4320 bool isWordChar (char ch) pure nothrow {
4321 return (ch.isalnum || ch == '_' || ch > 127);