1 /* Invisible Vector Library.
2 * simple FlexBox-based TUI engine
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, version 3 of the License ONLY.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 module iv
.egeditor
.editor
/*is aliced*/;
18 //version = egeditor_scan_time;
19 //version = egeditor_scan_time_to_file;
20 // this is idiocity, i should change my habits
21 //version = egeditor_record_movement_undo;
27 debug import iv
.vfs
.io
;
29 version(egeditor_scan_time
) import iv
.pxclock
;
31 version(egeditor_record_movement_undo
) {
32 public enum EgEditorMovementUndo
= true;
34 public enum EgEditorMovementUndo
= false;
38 // ////////////////////////////////////////////////////////////////////////// //
39 /// this interface is used to measure text for pixel-sized editor
40 abstract class EgTextMeter
{
41 int currofs
; /// x offset for current char (i.e. the last char that was passed to `advance()` should be drawn with this offset)
42 int currwdt
; /// current line width (including, the last char that was passed to `advance()`), preferably without trailing empty space between chars
43 int currheight
; /// current text height; keep this in sync with the current state; `reset` should set it to "default text height"
45 /// this should reset text width iterator (and curr* fields); tabsize > 0: process tabs as... well... tabs ;-); tabsize < 0: calculating height
46 abstract void reset (int tabsize
) nothrow;
48 /// advance text width iterator, fix all curr* fields
49 abstract void advance (dchar ch
, in ref GapBuffer
.HighState hs
) nothrow;
51 /// finish text iterator; it should NOT reset curr* fields!
52 /// WARNING: EditorEngine tries to call this after each `reset()`, but user code may not
53 abstract void finish () nothrow;
57 // ////////////////////////////////////////////////////////////////////////// //
58 /// highlighter should be able to work line-by-line
61 GapBuffer gb
; /// this will be set by EditorEngine on attaching
62 LineCache lc
; /// this will be set by EditorEngine on attaching
67 /// return true if highlighting for this line was changed
68 abstract bool fixLine (int line
);
70 /// mark line as "need rehighlighting" (and possibly other text too)
71 /// wasInsDel: some lines was inserted/deleted down the text
72 abstract void lineChanged (int line
, bool wasInsDel
);
76 // ////////////////////////////////////////////////////////////////////////// //
78 public final class GapBuffer
{
80 static align(1) struct HighState
{
82 ubyte kwtype
; // keyword number
83 ubyte kwidx
; // index in keyword
84 @property pure nothrow @safe @nogc {
85 ushort u16 () const { pragma(inline
, true); return cast(ushort)((kwidx
<<8)|kwtype
); }
86 short s16 () const { pragma(inline
, true); return cast(short)((kwidx
<<8)|kwtype
); }
87 void u16 (ushort v
) { pragma(inline
, true); kwtype
= v
&0xff; kwidx
= (v
>>8)&0xff; }
88 void s16 (short v
) { pragma(inline
, true); kwtype
= v
&0xff; kwidx
= (v
>>8)&0xff; }
97 enum MinGapSize
= 1024; // bytes in gap
98 enum GrowGran
= 65536; // must be power of 2
99 enum MinGapSizeSmall
= 64; // bytes in gap
100 enum GrowGranSmall
= 0x100; // must be power of 2
102 static assert(GrowGran
>= MinGapSize
);
103 static assert(GrowGranSmall
>= MinGapSizeSmall
);
105 @property uint MGS () const pure nothrow @safe @nogc { pragma(inline
, true); return (mSingleLine ? MinGapSizeSmall
: MinGapSize
); }
108 char* tbuf
; // text buffer
109 HighState
* hbuf
; // highlight buffer
110 uint tbused
; // not including gap
111 uint tbsize
; // including gap
112 uint tbmax
= 512*1024*1024+MinGapSize
; // maximum buffer size
113 uint gapstart
, gapend
; // tbuf[gapstart..gapend]; gap cannot be empty
114 uint bufferChangeCounter
; // will simply increase on each buffer change
116 static private bool xrealloc(T
) (ref T
* ptr
, ref uint cursize
, int newsize
, uint gran
) nothrow @trusted @nogc {
117 import core
.stdc
.stdlib
: realloc
;
119 uint nsz
= ((newsize
+gran
-1)/gran
)*gran
;
120 assert(nsz
>= newsize
);
121 T
* nb
= cast(T
*)realloc(ptr
, nsz
*T
.sizeof
);
122 if (nb
is null) return false;
130 void initTBuf (bool hadHBuf
) nothrow @nogc {
131 import core
.stdc
.stdlib
: free
, malloc
, realloc
;
132 assert(tbuf
is null);
133 assert(hbuf
is null);
134 immutable uint nsz
= (mSingleLine ? GrowGranSmall
: GrowGran
);
135 tbuf
= cast(char*)malloc(nsz
);
136 if (tbuf
is null) assert(0, "out of memory for text buffers");
137 // allocate highlight buffer if necessary
139 hbuf
= cast(HighState
*)malloc(nsz
*hbuf
[0].sizeof
);
140 if (hbuf
is null) assert(0, "out of memory for text buffers");
141 hbuf
[0..nsz
] = HighState
.init
;
149 // ensure that we can place a text of size `size` in buffer, and will still have at least MGS bytes free
150 // may change `tbsize`, but will not touch `tbused`
151 bool growTBuf (uint size
) nothrow @nogc {
152 if (size
> tbmax
) return false; // too big
153 immutable uint mingapsize
= MGS
; // desired gap buffer size
154 immutable uint unused
= tbsize
-tbused
; // number of unused bytes in buffer
155 assert(tbused
<= tbsize
);
156 if (size
<= tbused
&& unused
>= mingapsize
) return true; // nothing to do, we have enough room in buffer
157 // if the gap is bigger than the minimal gap size, check if we have enough extra bytes to avoid allocation
158 if (unused
> mingapsize
) {
159 immutable uint extra
= unused
-mingapsize
; // extra bytes we can spend
160 immutable uint bgrow
= size
-tbused
; // number of bytes we need
161 if (extra
>= bgrow
) return true; // yay, no need to realloc
164 immutable uint newsz
= size
+mingapsize
;
165 immutable uint gran
= (mSingleLine ? GrowGranSmall
: GrowGran
);
166 uint hbufsz
= tbsize
;
167 if (!xrealloc(tbuf
, tbsize
, newsz
, gran
)) return false;
168 // reallocate highlighting buffer only if we already have one
170 if (!xrealloc(hbuf
, hbufsz
, newsz
, gran
)) { tbsize
= hbufsz
; return false; } // HACK!
171 assert(tbsize
== hbufsz
);
173 assert(tbsize
>= newsz
);
178 uint pos2real (uint pos
) const pure @safe nothrow @nogc {
179 pragma(inline
, true);
180 return pos
+(pos
>= gapstart ? gapend
-gapstart
: 0);
184 HighState defhs
; /// default highlighting state for new text
188 this (bool asingleline
) nothrow @nogc {
189 mSingleLine
= asingleline
;
190 initTBuf(false); // don't allocate hbuf yet
194 ~this () nothrow @nogc {
195 import core
.stdc
.stdlib
: free
;
196 if (tbuf
!is null) free(tbuf
);
197 if (hbuf
!is null) free(hbuf
);
200 /// remove all text from buffer
201 /// WILL NOT call deletion hooks!
202 void clear () nothrow @nogc {
203 import core
.stdc
.stdlib
: free
;
204 immutable bool hadHBuf
= (hbuf
!is null);
205 if (tbuf
!is null) { free(tbuf
); tbuf
= null; }
206 if (hadHBuf
) { free(hbuf
); hbuf
= null; }
207 ++bufferChangeCounter
;
211 @property bool hasHiBuffer () const pure nothrow @safe @nogc { pragma(inline
, true); return (hbuf
!is null); } ///
213 /// after calling this with `true`, `hasHiBuffer` may still be false if there is no memory for it
214 @property void hasHiBuffer (bool v
) nothrow @trusted @nogc {
215 if (v
!= hasHiBuffer
) {
217 // create highlighting buffer
218 import core
.stdc
.stdlib
: malloc
;
219 assert(hbuf
is null);
221 hbuf
= cast(HighState
*)malloc(tbsize
*hbuf
[0].sizeof
);
222 if (hbuf
!is null) hbuf
[0..tbsize
] = HighState
.init
;
224 // remove highlighitng buffer
225 import core
.stdc
.stdlib
: free
;
226 assert(hbuf
!is null);
233 /// "single line" mode, for line editors
234 bool singleline () const pure @safe nothrow @nogc { pragma(inline
, true); return mSingleLine
; }
236 /// size of text buffer without gap, in one-byte chars
237 @property int textsize () const pure @safe nothrow @nogc { pragma(inline
, true); return tbused
; }
239 @property char opIndex (uint pos
) const pure @trusted nothrow @nogc { pragma(inline
, true); return (pos
< tbused ? tbuf
[pos
+(pos
>= gapstart ? gapend
-gapstart
: 0)] : '\n'); } ///
240 @property ref HighState
hi (uint pos
) pure @trusted nothrow @nogc { pragma(inline
, true); return (hbuf
!is null && pos
< tbused ? hbuf
[pos
+(pos
>= gapstart ? gapend
-gapstart
: 0)] : (hidummy
= hidummy
.init
)); } ///
242 @property dchar uniAt (uint pos
) const @trusted nothrow @nogc {
243 immutable ts
= tbused
;
244 if (pos
>= ts
) return '\n';
247 if (udc
.decodeSafe(cast(ubyte)tbuf
[pos2real(pos
++)])) return udc
.codepoint
;
249 return udc
.codepoint
;
252 @property dchar uniAtAndAdvance (ref int pos
) const @trusted nothrow @nogc {
253 immutable ts
= tbused
;
254 if (pos
< 0) pos
= 0;
255 if (pos
>= ts
) return '\n';
258 if (udc
.decodeSafe(cast(ubyte)tbuf
[pos2real(pos
++)])) return udc
.codepoint
;
260 return udc
.codepoint
;
263 /// return utf-8 character length at buffer position pos or -1 on error (or 1 on error if "always positive")
264 /// never returns zero
265 int utfuckLenAt(bool alwaysPositive
=true) (int pos
) const @trusted nothrow @nogc {
266 immutable ts
= tbused
;
267 if (pos
< 0 || pos
>= ts
) {
268 static if (alwaysPositive
) return 1; else return -1;
270 char ch
= tbuf
[pos2real(pos
)];
271 if (ch
< 128) return 1;
275 ch
= tbuf
[pos2real(pos
++)];
276 if (udc
.decode(cast(ubyte)ch
)) {
277 static if (alwaysPositive
) {
278 return (udc
.invalid ?
1 : pos
-spos
);
280 return (udc
.invalid ?
-1 : pos
-spos
);
284 static if (alwaysPositive
) return 1; else return -1;
287 // ensure that the buffer has room for at least one char in the gap
288 // note that this may move gap
289 protected void ensureGap () pure @safe nothrow @nogc {
290 pragma(inline
, true);
291 // if we have zero-sized gap, assume that it is at end; we always have a room for at least MinGapSize(Small) chars
292 if (gapstart
>= gapend || gapstart
>= tbused
) {
293 assert(tbused
<= tbsize
);
296 assert(gapend
-gapstart
>= MGS
);
300 /// put the gap *before* `pos`
301 void moveGapAtPos (int pos
) @trusted nothrow @nogc {
302 import core
.stdc
.string
: memmove
;
303 immutable ts
= tbused
; // i will need it in several places
304 if (pos
< 0) pos
= 0;
305 if (pos
> ts
) pos
= ts
;
306 if (ts
== 0) { gapstart
= 0; gapend
= tbsize
; return; } // unlikely case, but...
307 ensureGap(); // we should have a gap
309 * pos is before gap: shift [pos..gapstart] to gapend-len, shift gap
310 * pos is after gap: shift [gapend..pos] to gapstart, shift gap
312 if (pos
< gapstart
) {
314 int len
= gapstart
-pos
; // to shift
315 memmove(tbuf
+gapend
-len
, tbuf
+pos
, len
);
316 if (hbuf
!is null) memmove(hbuf
+gapend
-len
, hbuf
+pos
, len
*hbuf
[0].sizeof
);
319 } else if (pos
> gapstart
) {
321 int len
= pos
-gapstart
;
322 memmove(tbuf
+gapstart
, tbuf
+gapend
, len
);
323 if (hbuf
!is null) memmove(hbuf
+gapstart
, hbuf
+gapend
, len
*hbuf
[0].sizeof
);
327 // if we moved gap to buffer end, grow it; `ensureGap()` will do it for us
329 assert(gapstart
== pos
);
330 assert(gapstart
< gapend
);
331 assert(gapstart
<= ts
);
334 /// put the gap at the end of the text
335 void moveGapAtEnd () @trusted nothrow @nogc { moveGapAtPos(tbused
); }
337 /// put text into buffer; will either put all the text, or nothing
338 /// returns success flag
339 bool append (const(char)[] str...) @trusted nothrow @nogc { return (put(tbused
, str) >= 0); }
341 /// put text into buffer; will either put all the text, or nothing
342 /// returns new position or -1
343 int put (int pos
, const(char)[] str...) @trusted nothrow @nogc {
344 import core
.stdc
.string
: memcpy
;
345 if (pos
< 0) pos
= 0;
346 bool atend
= (pos
>= tbused
);
347 if (atend
) pos
= tbused
;
348 if (str.length
== 0) return pos
;
349 if (tbmax
-(tbsize
-tbused
) < str.length
) return -1; // no room
350 if (!growTBuf(tbused
+cast(uint)str.length
)) return -1; // memory allocation failed
351 //TODO: this can be made faster, but meh...
352 immutable slen
= cast(uint)str.length
;
353 if (atend || gapend
-gapstart
< slen
) moveGapAtEnd(); // this will grow the gap, so it will take all available room
354 if (!atend
) moveGapAtPos(pos
); // condition is used for tiny speedup
355 assert(gapend
-gapstart
>= slen
);
356 memcpy(tbuf
+gapstart
, str.ptr
, str.length
);
357 if (hbuf
!is null) hbuf
[gapstart
..gapstart
+str.length
] = defhs
;
362 assert(tbsize
-tbused
>= MGS
);
363 ++bufferChangeCounter
;
367 /// remove count bytes from the current position; will either remove all of 'em, or nothing
368 /// returns success flag
369 bool remove (int pos
, int count
) @trusted nothrow @nogc {
370 import core
.stdc
.string
: memmove
;
371 if (count
< 0) return false;
372 if (count
== 0) return true;
373 immutable ts
= tbused
; // cache current text size
374 if (pos
< 0) pos
= 0;
375 if (pos
> ts
) pos
= ts
;
376 if (ts
-pos
< count
) return false; // not enough text here
377 assert(gapstart
< gapend
);
378 ++bufferChangeCounter
; // buffer will definitely be changed
380 // at the start of the gap: i can just increase gap
381 if (pos
== gapstart
) {
386 // removing text just before gap: increase gap (backspace does this)
387 if (pos
+count
== gapstart
) {
390 assert(gapstart
== pos
);
393 // both variants failed; move gap at `pos` and try again
398 /// count how much eols we have in this range
399 int countEolsInRange (int pos
, int count
) const @trusted nothrow @nogc {
400 import core
.stdc
.string
: memchr
;
401 if (count
< 1 || pos
<= -count || pos
>= tbused
) return 0;
402 if (pos
+count
> tbused
) count
= tbused
-pos
;
405 int npos
= fastFindCharIn(pos
, count
, '\n')+1;
406 if (npos
<= 0) break;
414 /// using `memchr`, jumps over gap; never moves after `tbused`
415 public uint fastSkipEol (int pos
) const @trusted nothrow @nogc {
416 import core
.stdc
.string
: memchr
;
417 immutable ts
= tbused
;
418 if (ts
== 0 || pos
>= ts
) return ts
;
419 if (pos
< 0) pos
= 0;
420 // check text before gap
421 if (pos
< gapstart
) {
422 auto fp
= cast(char*)memchr(tbuf
+pos
, '\n', gapstart
-pos
);
423 if (fp
!is null) return cast(int)(fp
-tbuf
)+1;
424 pos
= gapstart
; // new starting position
426 assert(pos
>= gapstart
);
427 // check after gap and to text end
430 auto stx
= tbuf
+gapend
+(pos
-gapstart
);
431 assert(cast(usize
)(tbuf
+tbsize
-stx
) >= left
);
432 auto fp
= cast(char*)memchr(stx
, '\n', left
);
433 if (fp
!is null) return pos
+cast(int)(fp
-stx
)+1;
438 /// using `memchr`, jumps over gap; returns `tbused` if not found
439 public uint fastFindChar (int pos
, char ch
) const @trusted nothrow @nogc {
440 int res
= fastFindCharIn(pos
, tbused
, ch
);
441 return (res
>= 0 ? res
: tbused
);
444 /// use `memchr`, jumps over gap; returns -1 if not found
445 public int fastFindCharIn (int pos
, int len
, char ch
) const @trusted nothrow @nogc {
446 import core
.stdc
.string
: memchr
;
447 immutable ts
= tbused
;
448 if (len
< 1) return -1;
449 if (ts
== 0 || pos
>= ts
) return -1;
451 if (pos
<= -len
) return -1;
455 if (tbused
-pos
< len
) len
= tbused
-pos
;
458 // check text before gap
459 if (pos
< gapstart
) {
461 if (left
> len
) left
= len
;
462 auto fp
= cast(char*)memchr(tbuf
+pos
, ch
, left
);
463 if (fp
!is null) return cast(int)(fp
-tbuf
);
464 if ((len
-= left
) == 0) return -1;
465 pos
= gapstart
; // new starting position
467 assert(pos
>= gapstart
);
468 // check after gap and to text end
470 if (left
> len
) left
= len
;
472 auto stx
= tbuf
+gapend
+(pos
-gapstart
);
473 assert(cast(usize
)(tbuf
+tbsize
-stx
) >= left
);
474 auto fp
= cast(char*)memchr(stx
, ch
, left
);
475 if (fp
!is null) return pos
+cast(int)(fp
-stx
);
481 /// this is hack for regexp searchers
482 /// do not store returned slice anywhere for a long time!
483 /// slice *will* be invalidated on next gap buffer operation!
484 public auto bufparts (int pos
) nothrow @nogc {
485 static struct Range
{
488 bool aftergap
; // fr is "aftergap"?
490 private this (GapBuffer agb
, int pos
) {
492 auto ts
= agb
.tbused
;
493 if (ts
== 0 || pos
>= ts
) { gb
= null; return; }
494 if (pos
< 0) pos
= 0;
495 if (pos
< agb
.gapstart
) {
496 fr
= agb
.tbuf
[pos
..agb
.gapstart
];
499 if (left
< 1) { gb
= null; return; }
501 fr
= agb
.tbuf
[agb
.gapend
+pos
..agb
.gapend
+pos
+left
];
505 @property bool empty () pure const @safe { pragma(inline
, true); return (gb
is null); }
506 @property const(char)[] front () pure @safe { pragma(inline
, true); return fr
; }
508 if (aftergap
) gb
= null;
509 if (gb
is null) { fr
= null; return; }
510 int left
= gb
.textsize
-gb
.gapstart
;
511 if (left
< 1) { gb
= null; fr
= null; return; }
512 fr
= gb
.tbuf
[gb
.gapend
..gb
.gapend
+left
];
516 return Range(this, pos
);
519 /// this calls dg with continuous buffer parts, so you can write 'em to a file, for example
520 public final void forEachBufPart (int pos
, int len
, scope void delegate (const(char)[] buf
) dg
) {
521 if (dg
is null) return;
522 immutable ts
= tbused
;
524 if (ts
== 0 || pos
>= ts
) return;
526 if (pos
<= -len
) return;
532 // check text before gap
533 if (pos
< gapstart
) {
535 if (left
> len
) left
= len
;
537 dg(tbuf
[pos
..pos
+left
]);
538 if ((len
-= left
) == 0) return; // nothing more to do
539 pos
= gapstart
; // new starting position
541 assert(pos
>= gapstart
);
542 // check after gap and to text end
544 if (left
> len
) left
= len
;
546 auto stx
= tbuf
+gapend
+(pos
-gapstart
);
547 assert(cast(usize
)(tbuf
+tbsize
-stx
) >= left
);
554 static bool hasEols (const(char)[] str) pure nothrow @trusted @nogc {
555 import core
.stdc
.string
: memchr
;
556 uint left
= cast(uint)str.length
;
557 return (left
> 0 && memchr(str.ptr
, '\n', left
) !is null);
561 static int findEol (const(char)[] str) pure nothrow @trusted @nogc {
562 if (str.length
> int.max
) assert(0, "string too long");
563 int left
= cast(int)str.length
;
565 import core
.stdc
.string
: memchr
;
566 auto dp
= cast(const(char)*)memchr(str.ptr
, '\n', left
);
567 if (dp
!is null) return cast(int)(dp
-str.ptr
);
572 /// count number of '\n' chars in string
573 static int countEols (const(char)[] str) pure nothrow @trusted @nogc {
574 import core
.stdc
.string
: memchr
;
575 if (str.length
> int.max
) assert(0, "string too long");
577 uint left
= cast(uint)str.length
;
580 auto ep
= cast(const(char)*)memchr(dsp
, '\n', left
);
581 if (ep
is null) break;
584 left
-= cast(uint)(ep
-dsp
);
592 // ////////////////////////////////////////////////////////////////////////// //
593 // Self-Healing Line Cache (utm) implementation
594 //TODO(?): don't do full cache repairing
595 private final class LineCache
{
597 // line offset/height cache item
598 // to be safe, locache[mLineCount] is always valid and holds gb.textsize
599 static align(1) struct LOCItem
{
602 private uint mheight
; // 0: unknown; line height; high bit is reserved for "viswrap" flag
603 pure nothrow @safe @nogc:
604 @property bool validHeight () const { pragma(inline
, true); return ((mheight
&0x7fff_ffffU
) != 0); }
605 @property void resetHeight () { pragma(inline
, true); mheight
&= 0x8000_0000U; } // doesn't reset "viswrap" flag
606 @property uint height () const { pragma(inline
, true); return (mheight
&0x7fff_ffffU
); }
607 @property void height (uint v
) { pragma(inline
, true); assert(v
<= 0x7fff_ffffU
); mheight
= (mheight
&0x8000_0000)|v
; }
608 @property bool viswrap () const { pragma(inline
, true); return ((mheight
&0x8000_0000U) != 0); }
609 @property viswrap (bool v
) { pragma(inline
, true); if (v
) mheight |
= 0x8000_0000U; else mheight
&= 0x7fff_ffffU
; }
610 @property void resetHeightAndWrap () { pragma(inline
, true); mheight
= 0; }
611 @property void resetHeightAndSetWrap () { pragma(inline
, true); mheight
= 0x8000_0000U; }
612 void initWithPos (uint pos
) { pragma(inline
, true); ofs
= pos
; mheight
= 0; }
617 LOCItem
* locache
; // line info cache
618 uint locsize
; // number of allocated items in locache
619 uint mLineCount
; // total number of lines (and valid items in cache too)
620 EgTextMeter textMeter
; // null: monospaced font
621 int mLineHeight
= 1; // line height, in pixels/cells; <0: variable; 0: invalid state
622 dchar delegate (char ch
) nothrow recode1byte
; // not null: delegate to recode from 1bt to unishit
623 int mWordWrapWidth
= 0; // >0: "visual wrap"; either in pixels (if textMeter is set) or in cells
626 // utfuck-8 and visual tabs support
627 // WARNING! this will SIGNIFICANTLY slow down coordinate calculations!
628 bool utfuck
= false; /// should x coordinate calculation assume that the text is in UTF-8?
629 bool visualtabs
= false; /// should x coordinate calculation assume that tabs are not one-char width?
630 ubyte tabsize
= 2; /// tab size, in spaces
632 final: // just in case the compiler won't notice "final class"
634 void initLC () nothrow @nogc {
635 import core
.stdc
.stdlib
: realloc
;
636 // allocate initial line cache
637 uint ICS
= (gb
.mSingleLine ?
2 : 1024);
638 locache
= cast(typeof(locache
[0])*)realloc(locache
, ICS
*locache
[0].sizeof
);
639 if (locache
is null) assert(0, "out of memory for line cache");
640 locache
[0..ICS
] = LOCItem
.init
;
641 locache
[1].ofs
= gb
.textsize
; // just in case
643 mLineCount
= 1; // we always have at least one line, even if it is empty
646 // `total` is new number of entries in cache; actual number will be greater by one
647 bool growLineCache (uint total
) nothrow @nogc {
648 if (total
>= int.max
/8) assert(0, "egeditor: wtf?!");
649 ++total
; // for last item
650 if (locsize
< total
) {
651 // have to allocate more
652 if (!GapBuffer
.xrealloc(locache
, locsize
, total
, (locsize
< 4096 ?
0x400 : 0x1000))) return false;
657 int calcLineHeight (int lidx
) nothrow {
658 assert(lidx
>= 0 && lidx
< mLineCount
);
659 int ls
= locache
[lidx
].ofs
;
660 int le
= locache
[lidx
+1].ofs
;
661 if (locache
[lidx
].viswrap
) ++le
;
662 textMeter
.reset(-1); // nobody cares about tab widths here
663 scope(exit
) textMeter
.finish();
664 int maxh
= textMeter
.currheight
;
665 if (maxh
< 1) maxh
= 1;
666 auto tbufcopy
= gb
.tbuf
;
667 auto hbufcopy
= gb
.hbuf
;
668 gb
.hidummy
= GapBuffer
.HighState
.init
; // just in case
671 GapBuffer
.HighState
* hs
= (hbufcopy
!is null ? hbufcopy
+gb
.pos2real(ls
) : &gb
.hidummy
);
673 char ch
= tbufcopy
[gb
.pos2real(ls
++)];
674 if (udc
.decodeSafe(cast(ubyte)ch
)) {
675 immutable dchar dch
= udc
.codepoint
;
676 textMeter
.advance(dch
, *hs
);
678 if (textMeter
.currheight
> maxh
) maxh
= textMeter
.currheight
;
679 if (ls
< le
&& hbufcopy
!is null) hs
= hbufcopy
+gb
.pos2real(ls
);
682 auto rc1b
= recode1byte
;
684 immutable uint rpos
= gb
.pos2real(ls
++);
685 dchar dch
= (rc1b
!is null ?
rc1b(tbufcopy
[rpos
]) : cast(dchar)tbufcopy
[rpos
]);
686 GapBuffer
.HighState
* hs
= (hbufcopy
!is null ? hbufcopy
+rpos
: &gb
.hidummy
);
687 textMeter
.advance(dch
, *hs
);
688 if (textMeter
.currheight
> maxh
) maxh
= textMeter
.currheight
;
691 //{ import core.stdc.stdio; printf("line #%d height is %d\n", lidx, maxh); }
696 int findLineCacheIndex (uint pos
) const nothrow @nogc {
697 int lcount
= mLineCount
;
698 if (lcount
<= 0) return -1;
699 if (pos
>= gb
.tbused
) return lcount
-1;
700 if (lcount
== 1) return (pos
< locache
[1].ofs ?
0 : -1);
701 if (pos
< locache
[lcount
].ofs
) {
702 // yay! use binary search to find the line
703 int bot
= 0, i
= lcount
-1;
705 int mid
= i
-(i
-bot
)/2;
706 //!assert(mid >= 0 && mid < locused);
707 immutable ls
= locache
[mid
].ofs
;
708 immutable le
= locache
[mid
+1].ofs
;
709 if (pos
>= ls
&& pos
< le
) return mid
; // i found her!
710 if (pos
< ls
) i
= mid
-1; else bot
= mid
;
717 // get range for current wrapped line: [ltop..lbot)
718 void wwGetTopBot (int lidx
, int* ltop
, int* lbot
) const nothrow @trusted @nogc {
719 if (lidx
< 0) lidx
= 0;
720 if (lidx
>= mLineCount
) { *ltop
= *lbot
= mLineCount
; return; }
721 *ltop
= *lbot
= lidx
;
722 if (mWordWrapWidth
<= 0) { *lbot
+= 1; return; }
723 immutable bool curIsMiddle
= locache
[lidx
].viswrap
;
724 // find first (top) line move up to previous unwrapped, and then one down
725 if (lidx
> 0) while (lidx
> 0 && locache
[lidx
-1].viswrap
) --lidx
;
727 // find last (bottom) line (if current is wrapped, move down to first unwrapped; then one down anyway)
729 if (curIsMiddle
) while (lidx
< mLineCount
&& locache
[lidx
].viswrap
) ++lidx
;
730 if (++lidx
> mLineCount
) lidx
= mLineCount
; // just in case
732 assert(*ltop
< *lbot
);
735 int collapseWrappedLine (int lidx
) nothrow @nogc {
736 import core
.stdc
.string
: memmove
;
737 if (mWordWrapWidth
<= 0 || gb
.mSingleLine || lidx
< 0 || lidx
>= mLineCount
) return lidx
;
738 if (!locache
[lidx
].viswrap
) return lidx
; // early exit
740 wwGetTopBot(lidx
, <op
, &lbot
);
741 immutable int tokill
= lbot
-ltop
-1;
742 version(none
) { import core
.stdc
.stdio
; printf("collapsing: lidx=%d; ltop=%d; lbot=%d; tokill=%d; left=%d\n", lidx
, ltop
, lbot
, tokill
, mLineCount
-lbot
); }
743 if (tokill
<= 0) return lidx
; // nothing to do
744 // remove cache items for wrapped lines
745 if (lbot
<= mLineCount
) memmove(locache
+ltop
+1, locache
+lbot
, (mLineCount
-lbot
+1)*locache
[0].sizeof
);
747 mLineCount
-= tokill
;
748 // and fix wrapping flag
749 locache
[ltop
].resetHeightAndWrap();
753 // do word wrapping; line cache should be calculated and repaired for the given line
754 // (and down, if it was already wrapped)
755 // returns next line index to possible wrap
756 //TODO: "unwrap" line if word wrapping is not set?
757 //TODO: make it faster
758 //TODO: unicode blanks?
759 int doWordWrapping (int lidx
) nothrow {
760 import core
.stdc
.string
: memmove
;
762 static bool isBlank (char ch
) pure nothrow @safe @nogc { pragma(inline
, true); return (ch
== '\t' || ch
== ' '); }
764 immutable int www
= mWordWrapWidth
;
765 if (www
<= 0 || gb
.mSingleLine
) return mLineCount
;
766 if (lidx
< 0) lidx
= 0;
767 if (lidx
>= mLineCount
) return mLineCount
;
768 // find first and last line, if it was already wrapped
770 wwGetTopBot(lidx
, <op
, &lbot
);
772 if (textMeter
) textMeter
.reset(visualtabs ? tabsize
: 0);
773 scope(exit
) textMeter
.finish();
774 version(none
) { import core
.stdc
.stdio
; printf("lidx=%d; ltop=%d; lbot=%d, linecount=%d\n", lidx
, ltop
, lbot
, mLineCount
); }
775 // we have line range here; go, ninja, go
777 int cpos
= locache
[lidx
].ofs
;
779 int lastWordStartPos
= 0;
780 immutable bool utfuckmode
= utfuck
;
781 immutable int tabsz
= (visualtabs ? tabsize
: 0);
783 while (gb
[cpos
] != '\n') {
784 // go until cwdt allows us; but we have to have at least one char
785 int nwdt
; // "new" width with the current char
786 int lpos
= cpos
; // "last position"
788 if (tabsz
> 0 && gb
[cpos
] == '\t') {
790 nwdt
= ((cwdt
+tabsz
)/tabsz
)*tabsz
;
793 if (utfuckmode
) cpos
+= gb
.utfuckLenAt
!true(cpos
); else ++cpos
;
796 dchar dch
= (utfuckmode ? gb
.uniAtAndAdvance(cpos
) : recode1byte
is null ?
cast(dchar)gb
[cpos
++] : recode1byte(gb
[cpos
++]));
797 tm
.advance(dch
, gb
.hi(lpos
));
800 //{ import core.stdc.stdio; printf(" lidx=%d; lineofs=%d; linelen=%d; cwdt=%d; nwdt=%d; maxwdt=%d; lpos=%d; cpos=%d\n", lidx, locache[lidx].ofs, locache[lidx].len, cwdt, nwdt, www, lpos, cpos); }
801 // if we have at least one char in line, check if we should wrap here
802 if (lpos
> locache
[lidx
].ofs
) {
807 tm
.reset(visualtabs ? tabsize
: 0);
809 // if we have at least one word, wrap on it
810 if (lastWordStartPos
> 0) {
811 cpos
= lastWordStartPos
;
812 lastWordStartPos
= 0;
816 locache
[lidx
].resetHeightAndSetWrap();
819 // insert new cache record
820 growLineCache(mLineCount
+1);
821 if (lidx
<= mLineCount
) memmove(locache
+lidx
+1, locache
+lidx
, (mLineCount
-lidx
+1)*locache
[0].sizeof
);
826 locache
[lidx
] = LOCItem
.init
; // reset all, including height and wrapping
827 locache
[lidx
].ofs
= cpos
;
831 // check for word boundary
832 if (isBlank(gb
[lpos
]) && !isBlank(gb
[cpos
])) lastWordStartPos
= cpos
;
838 locache
[lidx
].resetHeightAndWrap();
839 // remove unused cache items
840 version(none
) { import core
.stdc
.stdio
; printf(" 00: lidx=%d; ltop=%d; lbot=%d, linecount=%d\n", lidx
, ltop
, lbot
, mLineCount
); }
842 //TODO: make it faster
843 ++lidx
; // to kill: [lidx..lbot)
844 immutable int tokill
= lbot
-lidx
;
846 version(none
) { import core
.stdc
.stdio
; printf(" xx: lidx=%d; ltop=%d; lbot=%d, linecount=%d; tokill=%d\n", lidx
, ltop
, lbot
, mLineCount
, tokill
); }
847 if (lbot
<= mLineCount
) memmove(locache
+lidx
, locache
+lbot
, (mLineCount
-lbot
+1)*locache
[0].sizeof
);
849 mLineCount
-= tokill
;
851 version(none
) { import core
.stdc
.stdio
; printf(" 01: lidx=%d; ltop=%d; lbot=%d, linecount=%d\n", lidx
, ltop
, lbot
, mLineCount
); }
852 assert(lidx
== lbot
);
853 assert(mLineCount
> 0);
854 assert(locache
[lbot
].ofs
== cpos
+(cpos
< gb
.textsize ?
1 : 0));
859 this (GapBuffer agb
) nothrow @nogc {
860 assert(agb
!is null);
865 ~this () nothrow @nogc {
866 if (locache
!is null) {
867 import core
.stdc
.stdlib
: free
;
873 /// there is always at least one line, so `linecount` is never zero
874 @property int linecount () const pure nothrow @safe @nogc { pragma(inline
, true); return mLineCount
; }
876 void clear () nothrow @nogc {
877 import core
.stdc
.stdlib
: free
;
880 if (locache
!is null) { free(locache
); locache
= null; }
881 // allocate new buffer
885 /** load file like this:
886 * if (!lc.resizeBuffer(filesize)) throw new Exception("memory?");
887 * scope(failure) lc.clear();
888 * fl.rawReadExact(lc.getBufferPtr[]);
889 * if (!lc.rebuild()) throw new Exception("memory?");
892 /// allocate text buffer for the text of the given size
893 bool resizeBuffer (uint newsize
) nothrow @nogc {
894 if (newsize
> gb
.tbmax
) return false;
896 //{ import core.stdc.stdio; printf("resizing buffer to %u bytes\n", newsize); }
897 if (!gb
.growTBuf(newsize
)) return false;
898 gb
.tbused
= gb
.gapstart
= newsize
;
899 gb
.gapend
= gb
.tbsize
;
904 /// get continuous buffer pointer, so we can read the whole file into it
905 char[] getBufferPtr () nothrow @nogc {
907 return gb
.tbuf
[0..gb
.textsize
];
910 /// count lines, fill line cache, do word wrapping
911 bool rebuild () nothrow {
912 import core
.stdc
.string
: memset
;
913 //gb.moveGapAtEnd(); // just in case
914 immutable ts
= gb
.textsize
;
915 const(char)* tb
= gb
.tbuf
;
916 if (gb
.mSingleLine
) {
921 locache
[0..2] = LOCItem
.init
;
925 version(egeditor_scan_time
) auto stt
= clockMilli();
927 // less memory fragmentation
928 int lcount
= gb
.countEolsInRange(0, ts
)+1; // total number of lines
929 //{ import core.stdc.stdio; printf("loaded %u bytes; %d lines found\n", gb.textsize, lcount); }
930 if (!growLineCache(lcount
)) return false;
931 assert(lcount
+1 <= locsize
);
932 //locache[0..lcount+1] = LOCItem.init; // reset all lines
933 memset(locache
, 0, (lcount
+1)*locache
[0].sizeof
); // reset all lines (help compiler a little)
935 LOCItem
* lcp
= locache
; // help compiler a little
936 foreach (immutable uint lidx
; 0..lcount
) {
937 lcp
.initWithPos(pos
);
939 pos
= gb
.fastSkipEol(pos
);
942 assert(lcp
is locache
+lcount
);
943 lcp
.ofs
= gb
.textsize
;
946 if (gb
.textsize
== 0) {
948 if (!growLineCache(1)) return false; // should have at least one
949 assert(locsize
>= 2);
950 locache
[0].initWithPos(0);
951 locache
[1].initWithPos(0);
954 int lcount
= 0; // total number of lines
955 //{ import core.stdc.stdio; printf("loaded %u bytes; %d lines found\n", gb.textsize, lcount); }
956 if (!growLineCache(1)) return false; // should have at least one
959 if (lcount
+1 >= locsize
) { if (!growLineCache(lcount
+1)) return false; }
960 locache
[lcount
++].initWithPos(pos
);
961 pos
= gb
.fastSkipEol(pos
);
964 // hack for last empty line, if it ends with '\n'
965 if (ts
&& gb
[ts
-1] == '\n') {
966 if (lcount
+1 >= locsize
) { if (!growLineCache(lcount
+1)) return false; }
967 locache
[lcount
++].initWithPos(ts
);
971 assert(lcount
< locsize
);
972 locache
[lcount
].initWithPos(pos
);
976 version(egeditor_scan_time
) {
977 import core
.stdc
.stdio
;
978 auto et
= clockMilli()-stt
;
979 version(egeditor_scan_time_to_file
) {
980 if (auto fo
= fopen("ztime.log", "a")) {
981 scope(exit
) fo
.fclose();
982 fo
.fprintf("%u lines (%u bytes) scanned in %u milliseconds\n", mLineCount
, gb
.textsize
, cast(uint)et
);
985 printf("%u lines (%u bytes) scanned in %u milliseconds\n", mLineCount
, gb
.textsize
, cast(uint)et
);
987 stt
= clockMilli(); // for wrapping
989 if (mWordWrapWidth
> 0) {
991 while (lidx
< mLineCount
) lidx
= doWordWrapping(lidx
);
992 version(egeditor_scan_time
) {
993 import core
.stdc
.stdio
;
994 et
= clockMilli()-stt
;
995 version(egeditor_scan_time_to_file
) {
996 if (auto fo
= fopen("ztime.log", "a")) {
997 scope(exit
) fo
.fclose();
998 fo
.fprintf(" %u lines wrapped in %u milliseconds\n", mLineCount
, cast(uint)et
);
1001 printf(" %u lines wrapped in %u milliseconds\n", mLineCount
, cast(uint)et
);
1008 /// put text into buffer; will either put all the text, or nothing
1009 /// returns success flag
1010 bool append (const(char)[] str...) nothrow { return (put(gb
.textsize
, str) >= 0); }
1012 /// put text into buffer; will either put all the text, or nothing
1013 /// returns new position or -1
1014 int put (int pos
, const(char)[] str...) nothrow {
1015 if (pos
< 0) pos
= 0;
1016 bool atend
= (pos
>= gb
.textsize
);
1017 if (str.length
== 0) return pos
;
1018 if (atend
) pos
= gb
.textsize
;
1019 auto ppos
= gb
.put(pos
, str);
1020 if (ppos
< 0) return ppos
;
1021 // heal line cache for single-line case
1022 if (gb
.mSingleLine
) {
1023 assert(mLineCount
== 1);
1024 assert(locsize
> 1);
1025 assert(locache
[0].ofs
== 0);
1026 locache
[1].ofs
= gb
.textsize
;
1027 locache
[0].resetHeightAndWrap();
1028 locache
[1].resetHeightAndWrap();
1031 int newlines
= GapBuffer
.countEols(str);
1032 immutable insertedLines
= newlines
;
1033 auto lidx
= findLineCacheIndex(pos
);
1034 immutable int ldelta
= ppos
-pos
;
1035 assert((!atend
&& lidx
>= 0) ||
(atend
&& (lidx
< 0 || lidx
== mLineCount
-1)));
1036 if (atend
) lidx
= mLineCount
-1;
1037 int wraplidx
= lidx
;
1038 if (newlines
== 0) {
1039 // no lines was inserted, just repair the length
1040 // no need to collapse wrapped line here, 'cause `doWordWrapping()` will rewrap it anyway
1041 locache
[lidx
++].resetHeight();
1043 import core
.stdc
.string
: memmove
;
1044 //FIXME: make this faster for wrapped lines
1045 wraplidx
= lidx
= collapseWrappedLine(wraplidx
);
1046 // we will start repairing from the last good line
1047 pos
= locache
[lidx
].ofs
;
1048 // inserted some new lines, make room for 'em
1049 growLineCache(mLineCount
+newlines
);
1050 if (lidx
<= mLineCount
) memmove(locache
+lidx
+newlines
, locache
+lidx
, (mLineCount
-lidx
+1)*locache
[0].sizeof
);
1051 mLineCount
+= newlines
;
1052 // no need to clear inserted lines, we'll overwrite em
1053 // recalc offsets and lengthes
1054 while (newlines
-- >= 0) {
1055 locache
[lidx
].ofs
= pos
;
1056 locache
[lidx
++].resetHeightAndWrap();
1057 pos
= gb
.fastSkipEol(pos
);
1060 // repair line cache (offsets) -- for now; switch to "repair on demand" later?
1061 if (lidx
<= mLineCount
) foreach (ref lc
; locache
[lidx
..mLineCount
+1]) lc
.ofs
+= ldelta
;
1062 if (mWordWrapWidth
> 0) {
1063 foreach (immutable c
; 0..insertedLines
+1) wraplidx
= doWordWrapping(wraplidx
);
1069 /// remove count bytes from the current position; will either remove all of 'em, or nothing
1070 /// returns success flag
1071 bool remove (int pos
, int count
) nothrow {
1072 if (gb
.mSingleLine
) {
1074 if (!gb
.remove(pos
, count
)) return false;
1075 assert(mLineCount
== 1);
1076 assert(locsize
> 1);
1077 assert(locache
[0].ofs
== 0);
1078 locache
[1].ofs
= gb
.textsize
;
1079 locache
[0].resetHeightAndWrap();
1080 locache
[1].resetHeightAndWrap();
1083 import core
.stdc
.string
: memmove
;
1084 if (count
< 0) return false;
1085 if (count
== 0) return true;
1086 if (pos
< 0) pos
= 0;
1087 if (pos
> gb
.textsize
) pos
= gb
.textsize
;
1088 if (gb
.textsize
-pos
< count
) return false; // not enough text here
1089 auto lidx
= findLineCacheIndex(pos
);
1091 int newlines
= gb
.countEolsInRange(pos
, count
);
1092 if (!gb
.remove(pos
, count
)) return false;
1093 // repair line cache
1094 if (newlines
== 0) {
1095 // no need to collapse wrapped line here, 'cause `doWordWrapping()` will rewrap it anyway
1096 locache
[lidx
].resetHeight();
1098 import core
.stdc
.string
: memmove
;
1099 if (mWordWrapWidth
> 0) {
1100 // collapse wordwrapped line into one, it is easier this way; it is safe to collapse lines in modified text
1101 //FIXME: make this faster for wrapped lines
1102 lidx
= collapseWrappedLine(lidx
);
1103 // collapse deleted lines too, so we can remove 'em as if they were normal ones
1104 foreach (immutable c
; 1..newlines
+1) collapseWrappedLine(lidx
+c
);
1106 // remove unused lines
1107 if (lidx
+1 <= mLineCount
) memmove(locache
+lidx
+1, locache
+lidx
+1+newlines
, (mLineCount
-lidx
)*locache
[0].sizeof
);
1108 mLineCount
-= newlines
;
1110 locache
[lidx
].resetHeightAndWrap();
1112 if (lidx
+1 <= mLineCount
) foreach (ref lc
; locache
[lidx
+1..mLineCount
+1]) lc
.ofs
-= count
;
1113 if (mWordWrapWidth
> 0) {
1114 lidx
= doWordWrapping(lidx
);
1115 // and next one, 'cause it was modified by collapser
1116 doWordWrapping(lidx
);
1122 int lineHeightPixels (int lidx
, bool forceRecalc
=false) nothrow {
1124 assert(textMeter
!is null);
1125 if (lidx
< 0 || mLineCount
== 0 || lidx
>= mLineCount
) {
1127 h
= (textMeter
.currheight
> 0 ? textMeter
.currheight
: 1);
1130 if (forceRecalc ||
!locache
[lidx
].validHeight
) locache
[lidx
].height
= calcLineHeight(lidx
);
1131 h
= locache
[lidx
].height
;
1136 bool isLastWrappedLine (int lidx
) nothrow @nogc { pragma(inline
, true); return (lidx
>= 0 && lidx
< mLineCount ?
!locache
[lidx
].viswrap
: true); }
1137 bool isWrappedLine (int lidx
) nothrow @nogc { pragma(inline
, true); return (lidx
>= 0 && lidx
< mLineCount ? locache
[lidx
].viswrap
: false); }
1139 /// get number of *symbols* to line end (this is not always equal to number of bytes for utfuck)
1140 int syms2eol (int pos
) nothrow {
1141 immutable ts
= gb
.textsize
;
1142 if (pos
< 0) pos
= 0;
1143 if (pos
>= ts
) return 0;
1144 int epos
= line2pos(pos2line(pos
)+1);
1145 if (!utfuck
) return epos
-pos
; // fast path
1148 while (pos
< epos
) {
1149 pos
+= gb
.utfuckLenAt
!true(pos
);
1155 /// get line for the given position
1156 int pos2line (int pos
) nothrow {
1157 immutable ts
= gb
.textsize
;
1158 if (pos
< 0) return 0;
1159 if (pos
== 0 || ts
== 0) return 0;
1160 if (pos
>= ts
) return mLineCount
-1; // end of text: no need to update line offset cache
1161 if (mLineCount
== 1) return 0;
1162 int lcidx
= findLineCacheIndex(pos
);
1163 assert(lcidx
>= 0 && lcidx
< mLineCount
);
1167 /// get position (starting) for the given line
1168 /// it will be 0 for negative lines, and `textsize` for positive out of bounds lines
1169 int line2pos (int lidx
) nothrow {
1170 if (lidx
< 0 || gb
.textsize
== 0) return 0;
1171 if (lidx
> mLineCount
-1) return gb
.textsize
;
1172 if (mLineCount
== 1) {
1176 return locache
[lidx
].ofs
;
1179 alias linestart
= line2pos
; /// ditto
1181 /// get ending position for the given line (position of '\n')
1182 /// it may be `textsize`, though, if this is the last line, and it doesn't end with '\n'
1183 int lineend (int lidx
) nothrow {
1184 if (lidx
< 0 || gb
.textsize
== 0) return 0;
1185 if (lidx
> mLineCount
-1) return gb
.textsize
;
1186 if (mLineCount
== 1) {
1190 if (lidx
== mLineCount
-1) return gb
.textsize
;
1191 auto res
= locache
[lidx
+1].ofs
;
1196 // move by `x` utfucked chars
1197 // `pos` should point to line start
1198 // will never go beyond EOL
1199 private int utfuck_x2pos (int x
, int pos
) nothrow {
1200 immutable ts
= gb
.textsize
;
1201 const(char)* tbuf
= gb
.tbuf
;
1202 if (pos
< 0) pos
= 0;
1203 if (mWordWrapWidth
<= 0 || gb
.mSingleLine
) {
1204 if (gb
.mSingleLine
) {
1206 while (pos
< ts
&& x
> 0) {
1207 pos
+= gb
.utfuckLenAt
!true(pos
); // "always positive"
1212 while (pos
< ts
&& x
> 0) {
1213 if (tbuf
[gb
.pos2real(pos
)] == '\n') break;
1214 pos
+= gb
.utfuckLenAt
!true(pos
); // "always positive"
1219 if (pos
>= ts
) return ts
;
1220 int lidx
= findLineCacheIndex(pos
);
1222 int epos
= locache
[lidx
+1].ofs
;
1223 while (pos
< epos
&& x
> 0) {
1224 if (tbuf
[gb
.pos2real(pos
)] == '\n') break;
1225 pos
+= gb
.utfuckLenAt
!true(pos
); // "always positive"
1229 if (pos
> ts
) pos
= ts
;
1233 // convert line offset to screen x coordinate
1234 // `pos` should point into line (somewhere)
1235 private int utfuck_pos2x(bool dotabs
=false) (int pos
) nothrow {
1236 immutable ts
= gb
.textsize
;
1237 if (pos
< 0) pos
= 0;
1238 if (pos
> ts
) pos
= ts
;
1239 immutable bool sl
= gb
.mSingleLine
;
1240 const(char)* tbuf
= gb
.tbuf
;
1242 if (mWordWrapWidth
<= 0) {
1246 while (spos
> 0 && tbuf
[gb
.pos2real(spos
-1)] != '\n') --spos
;
1250 // now `spos` points to line start; walk over utfucked chars
1251 while (spos
< pos
) {
1252 char ch
= tbuf
[gb
.pos2real(spos
)];
1253 if (!sl
&& ch
== '\n') break;
1254 static if (dotabs
) {
1255 if (ch
== '\t' && visualtabs
&& tabsize
> 0) {
1256 x
= ((x
+tabsize
)/tabsize
)*tabsize
;
1263 spos
+= (ch
< 128 ?
1 : gb
.utfuckLenAt
!true(spos
));
1266 // word-wrapped, eh...
1267 int lidx
= findLineCacheIndex(pos
);
1269 int spos
= locache
[lidx
].ofs
;
1270 int epos
= locache
[lidx
+1].ofs
;
1272 // now `spos` points to line start; walk over utfucked chars
1273 while (spos
< pos
) {
1274 char ch
= tbuf
[gb
.pos2real(spos
)];
1275 if (!sl
&& ch
== '\n') break;
1276 static if (dotabs
) {
1277 if (ch
== '\t' && visualtabs
&& tabsize
> 0) {
1278 x
= ((x
+tabsize
)/tabsize
)*tabsize
;
1285 spos
+= (ch
< 128 ?
1 : gb
.utfuckLenAt
!true(spos
));
1291 /// get position for the given text coordinates
1292 int xy2pos (int x
, int y
) nothrow {
1293 auto ts
= gb
.textsize
;
1294 if (ts
== 0 || y
< 0) return 0;
1295 if (y
> mLineCount
-1) return ts
;
1297 if (mLineCount
== 1) {
1299 return (!utfuck ?
(x
< ts ? x
: ts
) : utfuck_x2pos(x
, 0));
1301 uint ls
= locache
[y
].ofs
;
1302 uint le
= locache
[y
+1].ofs
;
1304 // this should be last empty line
1305 //if (y != mLineCount-1) { import std.format; assert(0, "fuuuuu; y=%u; lc=%u; locused=%u".format(y, mLineCount, locused)); }
1306 assert(y
== mLineCount
-1);
1310 // we want line end (except for last empty line, where we want end-of-text)
1311 if (x
>= le
-ls
) return (y
!= mLineCount
-1 ? le
-1 : le
);
1312 return ls
+x
; // somewhere in line
1315 return utfuck_x2pos(x
, ls
);
1319 /// get text coordinates for the given position
1320 void pos2xy (int pos
, out int x
, out int y
) nothrow {
1321 immutable ts
= gb
.textsize
;
1322 if (pos
<= 0 || ts
== 0) return; // x and y autoinited
1323 if (pos
> ts
) pos
= ts
;
1324 if (mLineCount
== 1) {
1326 x
= (!utfuck ? pos
: utfuck_pos2x(pos
));
1329 const(char)* tbuf
= gb
.tbuf
;
1331 // end of text: no need to update line offset cache
1333 if (!gb
.mSingleLine
) {
1334 while (pos
> 0 && tbuf
[gb
.pos2real(--pos
)] != '\n') ++x
;
1340 int lcidx
= findLineCacheIndex(pos
);
1341 assert(lcidx
>= 0 && lcidx
< mLineCount
);
1342 immutable ls
= locache
[lcidx
].ofs
;
1343 //auto le = lineofsc[lcidx+1];
1344 //!assert(pos >= ls && pos < le);
1345 y
= cast(uint)lcidx
;
1346 x
= (!utfuck ? pos
-ls
: utfuck_pos2x(pos
));
1349 /// get text coordinates (adjusted for tabs) for the given position
1350 void pos2xyVT (int pos
, out int x
, out int y
) nothrow {
1351 if (!utfuck
&& (!visualtabs || tabsize
== 0)) { pos2xy(pos
, x
, y
); return; }
1353 void tabbedX() (int ls
) {
1356 //TODO:FIXME: fix this!
1358 int tp
= fastFindCharIn(ls
, pos
-ls
, '\t');
1359 if (tp
< 0) { x
+= pos
-ls
; return; }
1362 while (ls
< pos
&& tbuf
[pos2real(ls
++)] == '\t') {
1363 x
= ((x
+tabsize
)/tabsize
)*tabsize
;
1367 const(char)* tbuf
= gb
.tbuf
;
1369 if (tbuf
[gb
.pos2real(ls
++)] == '\t') x
= ((x
+tabsize
)/tabsize
)*tabsize
; else ++x
;
1374 auto ts
= gb
.textsize
;
1375 if (pos
<= 0 || ts
== 0) return; // x and y autoinited
1376 if (pos
> ts
) pos
= ts
;
1377 if (mLineCount
== 1) {
1379 if (utfuck
) { x
= utfuck_pos2x
!true(pos
); return; }
1380 if (!visualtabs || tabsize
== 0) { x
= pos
; return; }
1385 // end of text: no need to update line offset cache
1386 const(char)* tbuf
= gb
.tbuf
;
1388 while (pos
> 0 && (gb
.mSingleLine || tbuf
[gb
.pos2real(--pos
)] != '\n')) ++x
;
1389 if (utfuck
) { x
= utfuck_pos2x
!true(ts
); return; }
1390 if (visualtabs
&& tabsize
!= 0) { int ls
= pos
+1; pos
= ts
; tabbedX(ls
); return; }
1393 int lcidx
= findLineCacheIndex(pos
);
1394 assert(lcidx
>= 0 && lcidx
< mLineCount
);
1395 auto ls
= locache
[lcidx
].ofs
;
1396 //auto le = lineofsc[lcidx+1];
1397 //!assert(pos >= ls && pos < le);
1398 y
= cast(uint)lcidx
;
1399 if (utfuck
) { x
= utfuck_pos2x
!true(pos
); return; }
1400 if (visualtabs
&& tabsize
> 0) { tabbedX(ls
); return; }
1406 // ////////////////////////////////////////////////////////////////////////// //
1407 private final class UndoStack
{
1412 CurMove
, // pos: old position; len: old topline (sorry)
1413 TextRemove
, // pos: position; len: length; deleted chars follows
1414 TextInsert
, // pos: position; len: length
1421 static align(1) struct Action
{
1424 BlockMarking
= 1<<0, // block marking state
1425 LastBE
= 1<<1, // last block move was at end?
1426 Changed
= 1<<2, // "changed" flag
1427 //VisTabs = 1<<3, // editor was in "visual tabs" mode
1428 CurAtEdge
= 1<<4, // cursor was at the block edge, use `edgex, edgey` to position it
1431 @property nothrow pure @safe @nogc {
1432 bool bmarking () const { pragma(inline
, true); return (flags
&Flag
.BlockMarking
) != 0; }
1433 bool lastbe () const { pragma(inline
, true); return (flags
&Flag
.LastBE
) != 0; }
1434 bool txchanged () const { pragma(inline
, true); return (flags
&Flag
.Changed
) != 0; }
1435 //bool vistabs () const { pragma(inline, true); return (flags&Flag.VisTabs) != 0; }
1436 bool curatedge () const { pragma(inline
, true); return (flags
&Flag
.CurAtEdge
) != 0; }
1438 void bmarking (bool v
) { pragma(inline
, true); if (v
) flags |
= Flag
.BlockMarking
; else flags
&= ~(Flag
.BlockMarking
); }
1439 void lastbe (bool v
) { pragma(inline
, true); if (v
) flags |
= Flag
.LastBE
; else flags
&= ~(Flag
.LastBE
); }
1440 void txchanged (bool v
) { pragma(inline
, true); if (v
) flags |
= Flag
.Changed
; else flags
&= ~(Flag
.Changed
); }
1441 //void vistabs (bool v) { pragma(inline, true); if (v) flags |= Flag.VisTabs; else flags &= ~Flag.VisTabs; }
1442 void curatedge (bool v
) { pragma(inline
, true); if (v
) flags |
= Flag
.CurAtEdge
; else flags
&= ~(Flag
.CurAtEdge
); }
1448 // after undoing action
1449 int cx
, cy
, topline
, xofs
;
1450 int bs
, be
; // block position
1451 int edgecurpos
, edgetopline
/*, edgexofs*/; // "edge" cursor postion
1457 version(Posix
) private import core
.sys
.posix
.unistd
: off_t
;
1460 version(Posix
) int tmpfd
= -1; else enum tmpfd
= -1;
1461 version(Posix
) off_t tmpsize
= 0;
1463 // undo buffer format:
1464 // last uint is always record size (not including size uints); record follows (up), then size again
1465 uint maxBufSize
= 32*1024*1024;
1467 uint ubUsed
, ubSize
;
1471 version(Posix
) void initTempFD () nothrow {
1472 import core
.sys
.posix
.fcntl
/*: open*/;
1473 static if (is(typeof(O_CLOEXEC
)) && is(typeof(O_TMPFILE
))) {
1474 auto xfd
= open("/tmp/_egundoz", O_RDWR|O_CLOEXEC|O_TMPFILE
, 0x1b6/*0o600*/);
1475 if (xfd
< 0) return;
1481 // returns record size
1482 version(Posix
) uint loadLastRecord(bool fullrecord
=true) (bool dropit
=false) nothrow {
1483 import core
.stdc
.stdio
: SEEK_SET
, SEEK_END
;
1484 import core
.sys
.posix
.unistd
: lseek
, read
;
1487 if (tmpsize
< sz
.sizeof
) return 0;
1488 lseek(tmpfd
, tmpsize
-sz
.sizeof
, SEEK_SET
);
1489 if (read(tmpfd
, &sz
, sz
.sizeof
) != sz
.sizeof
) return 0;
1490 if (tmpsize
< sz
+sz
.sizeof
*2) return 0;
1491 if (sz
< Action
.sizeof
) return 0;
1492 lseek(tmpfd
, tmpsize
-sz
-sz
.sizeof
, SEEK_SET
);
1493 static if (fullrecord
) {
1496 auto rsz
= cast(uint)Action
.sizeof
;
1499 import core
.stdc
.stdlib
: realloc
;
1500 auto nb
= cast(ubyte*)realloc(undoBuffer
, rsz
);
1501 if (nb
is null) return 0;
1506 if (read(tmpfd
, undoBuffer
, rsz
) != rsz
) return 0;
1507 if (dropit
) tmpsize
-= sz
+sz
.sizeof
*2;
1511 bool saveLastRecord () nothrow {
1513 import core
.stdc
.stdio
: SEEK_SET
;
1514 import core
.sys
.posix
.unistd
: lseek
, write
;
1516 assert(ubUsed
>= Action
.sizeof
);
1518 import core
.stdc
.stdlib
: free
;
1519 if (ubUsed
> 65536) {
1522 ubUsed
= ubSize
= 0;
1525 auto ofs
= lseek(tmpfd
, tmpsize
, SEEK_SET
);
1526 if (write(tmpfd
, &ubUsed
, ubUsed
.sizeof
) != ubUsed
.sizeof
) return false;
1527 if (write(tmpfd
, undoBuffer
, ubUsed
) != ubUsed
) return false;
1528 if (write(tmpfd
, &ubUsed
, ubUsed
.sizeof
) != ubUsed
.sizeof
) return false;
1529 write(tmpfd
, &tmpsize
, tmpsize
.sizeof
);
1530 tmpsize
+= ubUsed
+uint.sizeof
*2;
1536 // return `true` if something was removed
1537 bool removeFirstUndo () nothrow {
1538 import core
.stdc
.string
: memmove
;
1539 version(Posix
) assert(tmpfd
< 0);
1540 if (ubUsed
== 0) return false;
1541 uint np
= (*cast(uint*)undoBuffer
)+4*2;
1542 assert(np
<= ubUsed
);
1543 if (np
== ubUsed
) { ubUsed
= 0; return true; }
1544 memmove(undoBuffer
, undoBuffer
+np
, ubUsed
-np
);
1549 // return `null` if it can't; undo buffer is in invalid state then
1550 Action
* addUndo (int dataSize
) nothrow {
1551 import core
.stdc
.stdlib
: realloc
;
1552 import core
.stdc
.string
: memset
;
1553 version(Posix
) if (tmpfd
< 0) {
1554 if (dataSize
< 0 || dataSize
>= maxBufSize
) return null; // no room
1555 uint asz
= cast(uint)Action
.sizeof
+dataSize
+4*2;
1556 if (asz
> maxBufSize
) return null;
1557 if (ubSize
-ubUsed
< asz
) {
1558 uint nasz
= ubUsed
+asz
;
1559 if (nasz
&0xffff) nasz
= (nasz|
0xffff)+1;
1560 if (nasz
> maxBufSize
) {
1561 while (ubSize
-ubUsed
< asz
) { if (!removeFirstUndo()) return null; }
1563 auto nb
= cast(ubyte*)realloc(undoBuffer
, nasz
);
1565 while (ubSize
-ubUsed
< asz
) { if (!removeFirstUndo()) return null; }
1572 assert(ubSize
-ubUsed
>= asz
);
1573 *cast(uint*)(undoBuffer
+ubUsed
) = asz
-4*2;
1574 auto res
= cast(Action
*)(undoBuffer
+ubUsed
+4);
1575 *cast(uint*)(undoBuffer
+ubUsed
+asz
-4) = asz
-4*2;
1577 memset(res
, 0, asz
-4*2);
1582 if (dataSize
< 0 || dataSize
>= int.max
/4) return null; // wtf?!
1583 uint asz
= cast(uint)Action
.sizeof
+dataSize
;
1585 auto nb
= cast(ubyte*)realloc(undoBuffer
, asz
);
1586 if (nb
is null) return null;
1591 auto res
= cast(Action
*)undoBuffer
;
1592 memset(res
, 0, asz
);
1598 Action
* lastUndoHead () nothrow {
1599 version(Posix
) if (tmpfd
>= 0) {
1600 if (loadLastRecord
!false()) return null;
1601 return cast(Action
*)undoBuffer
;
1604 if (ubUsed
== 0) return null;
1605 auto sz
= *cast(uint*)(undoBuffer
+ubUsed
-4);
1606 return cast(Action
*)(undoBuffer
+ubUsed
-4-sz
);
1610 Action
* popUndo () nothrow {
1611 version(Posix
) if (tmpfd
>= 0) {
1612 auto len
= loadLastRecord
!true(true); // pop it
1613 return (len ?
cast(Action
*)undoBuffer
: null);
1616 if (ubUsed
== 0) return null;
1617 auto sz
= *cast(uint*)(undoBuffer
+ubUsed
-4);
1618 auto res
= cast(Action
*)(undoBuffer
+ubUsed
-4-sz
);
1625 this (bool aAsRich
, bool aAsRedo
, bool aIntoFile
) nothrow {
1631 //version(aliced) { import iv.rawtty; ttyBeep(); }
1637 import core
.stdc
.stdlib
: free
;
1638 import core
.sys
.posix
.unistd
: close
;
1639 if (tmpfd
>= 0) { close(tmpfd
); tmpfd
= -1; }
1640 if (undoBuffer
!is null) free(undoBuffer
);
1643 void clear (bool doclose
=false) nothrow {
1647 import core
.stdc
.stdlib
: free
;
1649 import core
.sys
.posix
.unistd
: close
;
1654 if (undoBuffer
!is null) free(undoBuffer
);
1658 if (ubSize
> 65536) {
1659 import core
.stdc
.stdlib
: realloc
;
1660 auto nb
= cast(ubyte*)realloc(undoBuffer
, 65536);
1666 version(Posix
) if (tmpfd
>= 0) tmpsize
= 0;
1670 void alwaysChanged () nothrow {
1673 while (pos
< ubUsed
) {
1674 auto sz
= *cast(uint*)(undoBuffer
+pos
);
1675 auto res
= cast(Action
*)(undoBuffer
+pos
+4);
1678 case Type
.TextRemove
:
1679 case Type
.TextInsert
:
1680 res
.txchanged
= true;
1687 import core
.stdc
.stdio
: SEEK_SET
;
1688 import core
.sys
.posix
.unistd
: lseek
, read
, write
;
1691 while (cpos
< tmpsize
) {
1693 lseek(tmpfd
, cpos
, SEEK_SET
);
1694 if (read(tmpfd
, &sz
, sz
.sizeof
) != sz
.sizeof
) break;
1695 if (sz
< Action
.sizeof
) assert(0, "wtf?!");
1696 if (read(tmpfd
, &act
, Action
.sizeof
) != Action
.sizeof
) break;
1698 case Type
.TextRemove
:
1699 case Type
.TextInsert
:
1700 if (act
.txchanged
!= true) {
1701 act
.txchanged
= true;
1702 lseek(tmpfd
, cpos
+sz
.sizeof
, SEEK_SET
);
1703 write(tmpfd
, &act
, Action
.sizeof
);
1708 cpos
+= sz
+sz
.sizeof
*2;
1714 private void fillCurPos (Action
* ua
, EditorEngine ed
) nothrow {
1715 if (ua
!is null && ed
!is null) {
1716 //TODO: correct x according to "visual tabs" mode (i.e. make it "normal x")
1719 ua
.topline
= ed
.mTopLine
;
1723 ua
.bmarking
= ed
.markingBlock
;
1724 ua
.lastbe
= ed
.lastBGEnd
;
1725 ua
.txchanged
= ed
.txchanged
;
1727 ua
.curatedge
= false;
1728 //ua.edgexofs = ed.mXOfs;
1729 //ua.vistabs = ed.visualtabs;
1730 // check if the cursor is at the edge of the block
1731 if (!asRedo
&& (ua
.type
== Type
.TextInsert || ua
.type
== Type
.TextRemove
)) {
1732 auto cpos
= ed
.curpos
;
1733 ua
.edgecurpos
= cpos
;
1734 ua
.edgetopline
= ua
.topline
;
1735 if (ua
.type
== Type
.TextInsert
) {
1736 // inserting text, is cursor at the insert position?
1737 if (cpos
== ua
.pos
) {
1738 // position it at the end, because undo will remove this text
1739 ua
.curatedge
= true;
1740 ua
.edgecurpos
= ua
.pos
+ua
.len
;
1742 } else if (ua
.type
== Type
.TextRemove
) {
1743 // deleting text, at the beginning, inside, or immediately after?
1744 if (cpos
>= ua
.pos
&& cpos
<= ua
.pos
+ua
.len
) {
1745 // position it at the start, because undo will insert this text
1746 ua
.curatedge
= true;
1747 ua
.edgecurpos
= ua
.pos
;
1754 version(egeditor_record_movement_undo
)
1755 bool addCurMove (EditorEngine ed
, bool fromRedo
=false) nothrow {
1756 if (auto lu
= lastUndoHead()) {
1757 if (lu
.type
== Type
.CurMove
) {
1758 if (lu
.cx
== ed
.cx
&& lu
.cy
== ed
.cy
&& lu
.topline
== ed
.mTopLine
&& lu
.xofs
== ed
.mXOfs
&&
1759 lu
.bs
== ed
.bstart
&& lu
.be
== ed
.bend
&& lu
.bmarking
== ed
.markingBlock
&&
1760 lu
.lastbe
== ed
.lastBGEnd
/*&& lu.vistabs == ed.visualtabs*/) return true;
1763 if (!asRedo
&& !fromRedo
&& ed
.redo
!is null) ed
.redo
.clear();
1764 auto act
= addUndo(0);
1765 if (act
is null) { clear(); return false; }
1766 act
.type
= Type
.CurMove
;
1767 fillCurPos(act
, ed
);
1768 return saveLastRecord();
1771 bool addTextRemove (EditorEngine ed
, int pos
, int count
, bool fromRedo
=false) nothrow {
1772 if (!asRedo
&& !fromRedo
&& ed
.redo
!is null) ed
.redo
.clear();
1773 GapBuffer gb
= ed
.gb
;
1774 assert(gb
!is null);
1775 if (pos
< 0 || pos
>= gb
.textsize
) return true;
1776 if (count
< 1) return true;
1777 if (count
>= maxBufSize
) { clear(); return false; }
1778 if (count
> gb
.textsize
-pos
) { clear(); return false; }
1779 int realcount
= count
;
1780 if (asRich
&& realcount
> 0) {
1781 if (realcount
>= int.max
/gb
.hbuf
[0].sizeof
/2) return false;
1782 realcount
+= realcount
*cast(int)gb
.hbuf
[0].sizeof
;
1784 auto act
= addUndo(realcount
);
1785 if (act
is null) { clear(); return false; }
1786 act
.type
= Type
.TextRemove
;
1789 fillCurPos(act
, ed
);
1790 auto dp
= act
.data
.ptr
;
1791 while (count
--) *dp
++ = gb
[pos
++];
1792 // save attrs for rich editor
1793 if (asRich
&& realcount
> 0) {
1796 auto dph
= cast(GapBuffer
.HighState
*)dp
;
1797 while (count
--) *dph
++ = gb
.hi(pos
++);
1799 return saveLastRecord();
1802 bool addTextInsert (EditorEngine ed
, int pos
, int count
, bool fromRedo
=false) nothrow {
1803 if (!asRedo
&& !fromRedo
&& ed
.redo
!is null) ed
.redo
.clear();
1804 auto act
= addUndo(0);
1805 if (act
is null) { clear(); return false; }
1806 act
.type
= Type
.TextInsert
;
1809 fillCurPos(act
, ed
);
1810 return saveLastRecord();
1813 bool addGroupStart (EditorEngine ed
, bool fromRedo
=false) nothrow {
1814 if (!asRedo
&& !fromRedo
&& ed
.redo
!is null) ed
.redo
.clear();
1815 auto act
= addUndo(0);
1816 if (act
is null) { clear(); return false; }
1817 act
.type
= Type
.GroupStart
;
1818 fillCurPos(act
, ed
);
1819 return saveLastRecord();
1822 bool addGroupEnd (EditorEngine ed
, bool fromRedo
=false) nothrow {
1823 if (!asRedo
&& !fromRedo
&& ed
.redo
!is null) ed
.redo
.clear();
1824 auto act
= addUndo(0);
1825 if (act
is null) { clear(); return false; }
1826 act
.type
= Type
.GroupEnd
;
1827 fillCurPos(act
, ed
);
1828 return saveLastRecord();
1831 @property bool hasUndo () const pure nothrow @safe @nogc { pragma(inline
, true); return (tmpfd
< 0 ?
(ubUsed
> 0) : (tmpsize
> 0)); }
1833 private bool copyAction (Action
* ua
) nothrow {
1834 import core
.stdc
.string
: memcpy
;
1835 if (ua
is null) return true;
1836 auto na
= addUndo(ua
.type
== Type
.TextRemove ? ua
.len
: 0);
1837 if (na
is null) return false;
1838 memcpy(na
, ua
, Action
.sizeof
+(ua
.type
== Type
.TextRemove ? ua
.len
: 0));
1839 return saveLastRecord();
1842 // return "None" in case of error
1843 Type
undoAction (EditorEngine ed
) {
1844 UndoStack oppos
= (asRedo ? ed
.undo
: ed
.redo
);
1845 assert(ed
!is null);
1846 version(egeditor_record_movement_undo
) {
1847 auto ua
= popUndo();
1849 // actions are done in two way: first, move cursor to where it should be, then perform the action
1850 auto ua
= lastUndoHead();
1852 if (ua
is null) return Type
.None
;
1853 //debug(egauto) if (!asRedo) { { import iv.vfs; auto fo = VFile("z00_undo.bin", "a"); fo.writeln(*ua); } }
1855 version(egeditor_record_movement_undo
) {
1856 // nothing interesting here
1858 // move cursor to the correct position first
1859 if (!asRedo
&& ua
.curatedge
&& (res
== Type
.TextInsert || res
== Type
.TextRemove
)) {
1860 // just in case, reset "curatedge"
1861 ua
.curatedge
= false;
1862 auto cpos
= ed
.curpos
;
1863 if (cpos
!= ua
.edgecurpos
) {
1864 // reposition the cursor
1865 ed
.markingBlock
= false;
1867 ed
.pos2xy(ua
.edgecurpos
, out cpx
, out cpy
);
1870 ed
.mTopLine
= ua
.edgetopline
;
1871 //ed.mXOfs = ua.edgexofs; //???
1872 ed
.makeCurLineVisible();
1873 return Type
.CurMove
; // yeah
1876 // otherwise, pop and perform the action
1878 assert(ua
!is null);
1880 final switch (ua
.type
) {
1881 case Type
.None
: assert(0, "wtf?!");
1882 case Type
.GroupStart
:
1884 if (oppos
!is null) { if (!oppos
.copyAction(ua
)) oppos
.clear(); }
1887 version(egeditor_record_movement_undo
) {
1888 if (oppos
!is null) { if (oppos
.addCurMove(ed
, asRedo
) == Type
.None
) oppos
.clear(); }
1891 assert(0, "egeditor undo: this should never happen");
1893 case Type
.TextInsert
: // remove inserted text
1894 if (oppos
!is null) { if (oppos
.addTextRemove(ed
, ua
.pos
, ua
.len
, asRedo
) == Type
.None
) oppos
.clear(); }
1895 //ed.writeLogAction(ua.pos, -ua.len);
1896 ed
.ubTextRemove(ua
.pos
, ua
.len
);
1898 case Type
.TextRemove
: // insert removed text
1899 if (oppos
!is null) { if (oppos
.addTextInsert(ed
, ua
.pos
, ua
.len
, asRedo
) == Type
.None
) oppos
.clear(); }
1900 if (ed
.ubTextInsert(ua
.pos
, ua
.data
.ptr
[0..ua
.len
])) {
1901 if (asRich
) ed
.ubTextSetAttrs(ua
.pos
, (cast(GapBuffer
.HighState
*)(ua
.data
.ptr
+ua
.len
))[0..ua
.len
]);
1903 //ed.writeLogAction(ua.pos, ua.len);
1906 //FIXME: optimize redraw
1907 if (ua
.bs
!= ed
.bstart || ua
.be
!= ed
.bend
) {
1908 if (ua
.bs
< ua
.be
) {
1910 if (ed
.bstart
< ed
.bend
) ed
.markLinesDirtySE(ed
.lc
.pos2line(ed
.bstart
), ed
.lc
.pos2line(ed
.bend
)); // old block is dirty
1911 ed
.markLinesDirtySE(ed
.lc
.pos2line(ua
.bs
), ed
.lc
.pos2line(ua
.be
)); // new block is dirty
1913 // undo has no block
1914 if (ed
.bstart
< ed
.bend
) ed
.markLinesDirtySE(ed
.lc
.pos2line(ed
.bstart
), ed
.lc
.pos2line(ed
.bend
)); // old block is dirty
1919 ed
.markingBlock
= ua
.bmarking
;
1920 ed
.lastBGEnd
= ua
.lastbe
;
1921 // don't restore "visual tabs" mode
1922 //TODO: correct x according to "visual tabs" mode (i.e. make it "visual x")
1925 ed
.mTopLine
= ua
.topline
;
1927 ed
.txchanged
= ua
.txchanged
;
1933 // ////////////////////////////////////////////////////////////////////////// //
1934 /// main editor engine: does all the undo/redo magic, block management, etc.
1935 class EditorEngine
{
1938 enum CodePage
: ubyte {
1943 CodePage codepage
= CodePage
.koi8u
; ///
1945 /// from koi to codepage
1946 final char recodeCharTo (char ch
) pure const nothrow {
1947 pragma(inline
, true);
1949 codepage
== CodePage
.cp1251 ?
uni2cp1251(koi2uni(ch
)) :
1950 codepage
== CodePage
.cp866 ?
uni2cp866(koi2uni(ch
)) :
1954 /// from codepage to koi
1955 final char recodeCharFrom (char ch
) pure const nothrow {
1956 pragma(inline
, true);
1958 codepage
== CodePage
.cp1251 ?
uni2koi(cp12512uni(ch
)) :
1959 codepage
== CodePage
.cp866 ?
uni2koi(cp8662uni(ch
)) :
1963 /// recode to codepage
1964 final char recodeU2B (dchar dch
) pure const nothrow {
1965 final switch (codepage
) {
1966 case CodePage
.koi8u
: return uni2koi(dch
);
1967 case CodePage
.cp1251
: return uni2cp1251(dch
);
1968 case CodePage
.cp866
: return uni2cp866(dch
);
1972 /// should not be called for utfuck mode
1973 final dchar recode1b (char ch
) pure const nothrow {
1974 final switch (codepage
) {
1975 case CodePage
.koi8u
: return koi2uni(ch
);
1976 case CodePage
.cp1251
: return cp12512uni(ch
);
1977 case CodePage
.cp866
: return cp8662uni(ch
);
1982 int lineHeightPixels
= 0; /// <0: use line height API, proportional fonts; 0: everything is cell-based (tty); >0: constant line height in pixels; proprotional fonts
1983 int prevTopLine
= -1;
1988 int[] dirtyLines
; // line heights or 0 if not dirty; hack!
1989 int winx
, winy
, winw
, winh
;
1993 UndoStack undo
, redo
;
1994 int bstart
= -1, bend
= -1; // marked block position
1996 bool lastBGEnd
; // last block grow was at end?
1998 bool mReadOnly
; // has any effect only if you are using `insertText()` and `deleteText()` API!
1999 bool mSingleLine
; // has any effect only if you are using `insertText()` and `deleteText()` API!
2000 bool mKillTextOnChar
; // mostly for single-line: remove all text on new char; will autoreset on move
2002 char[] indentText
; // this buffer is actively reused, do not expose!
2005 bool mAsRich
; /// this is "rich editor", so engine should save/restore highlighting info in undo
2008 bool[int] linebookmarked
; /// is this line bookmarked?
2011 //EgTextMeter textMeter; /// *MUST* be set when `inPixels` is true
2012 final @property EgTextMeter
textMeter () nothrow @nogc { return lc
.textMeter
; }
2013 final @property void textMeter (EgTextMeter tm
) nothrow @nogc { lc
.textMeter
= tm
; }
2015 final @property int wordWrapPos () const nothrow @nogc { pragma(inline
, true); return lc
.mWordWrapWidth
; }
2016 final @property void wordWrapPos (int v
) nothrow {
2018 if (lc
.mWordWrapWidth
!= v
) {
2020 lc
.mWordWrapWidth
= v
;
2027 /// is editor in "paste mode" (i.e. we are pasting chars from clipboard, and should skip autoindenting)?
2028 final @property bool pasteMode () const pure nothrow @safe @nogc { return (inPasteMode
> 0); }
2029 final resetPasteMode () pure nothrow @safe @nogc { inPasteMode
= 0; } /// reset "paste mode"
2032 void clearBookmarks () nothrow { linebookmarked
.clear(); }
2034 enum BookmarkChangeMode
{ Toggle
, Set
, Reset
} ///
2037 void bookmarkChange (int cy
, BookmarkChangeMode mode
) nothrow {
2038 if (cy
< 0 || cy
>= lc
.linecount
) return;
2039 if (mSingleLine
) return; // ignore for single-line mode
2040 final switch (mode
) {
2041 case BookmarkChangeMode
.Toggle
:
2042 if (cy
in linebookmarked
) linebookmarked
.remove(cy
); else linebookmarked
[cy
] = true;
2043 markLinesDirty(cy
, 1);
2045 case BookmarkChangeMode
.Set
:
2046 if (cy
!in linebookmarked
) {
2047 linebookmarked
[cy
] = true;
2048 markLinesDirty(cy
, 1);
2051 case BookmarkChangeMode
.Reset
:
2052 if (cy
in linebookmarked
) {
2053 linebookmarked
.remove(cy
);
2054 markLinesDirty(cy
, 1);
2061 final void doBookmarkToggle () nothrow { pragma(inline
, true); bookmarkChange(cy
, BookmarkChangeMode
.Toggle
); }
2064 final @property bool isLineBookmarked (int lidx
) nothrow {
2065 pragma(inline
, true);
2066 return ((lidx
in linebookmarked
) !is null);
2070 final void doBookmarkJumpUp () nothrow {
2072 foreach (int lidx
; linebookmarked
.byKey
) {
2073 if (lidx
< cy
&& lidx
> bestBM
) bestBM
= lidx
;
2076 version(egeditor_record_movement_undo
) pushUndoCurPos();
2080 makeCurLineVisibleCentered();
2085 final void doBookmarkJumpDown () nothrow {
2086 int bestBM
= int.max
;
2087 foreach (int lidx
; linebookmarked
.byKey
) {
2088 if (lidx
> cy
&& lidx
< bestBM
) bestBM
= lidx
;
2090 if (bestBM
< lc
.linecount
) {
2091 version(egeditor_record_movement_undo
) pushUndoCurPos();
2095 makeCurLineVisibleCentered();
2099 ///WARNING! don't mutate bookmarks here!
2100 final void forEachBookmark (scope void delegate (int lidx
) dg
) {
2101 if (dg
is null) return;
2102 foreach (int lidx
; linebookmarked
.byKey
) dg(lidx
);
2105 /// call this from `willBeDeleted()` (only!) to fix bookmarks
2106 final void bookmarkDeletionFix (int pos
, int len
, int eolcount
) nothrow {
2107 if (eolcount
&& linebookmarked
.length
> 0) {
2108 import core
.stdc
.stdlib
: malloc
, free
;
2109 // remove bookmarks whose lines are removed, move other bookmarks
2110 auto py
= lc
.pos2line(pos
);
2111 auto ey
= lc
.pos2line(pos
+len
);
2112 bool wholeFirstLineDeleted
= (pos
== lc
.line2pos(py
)); // do we want to remove the whole first line?
2113 bool wholeLastLineDeleted
= (pos
+len
== lc
.line2pos(ey
)); // do we want to remove the whole last line?
2114 if (wholeLastLineDeleted
) --ey
; // yes, `ey` is one line down the last, fix it
2115 // build new bookmark array
2116 int* newbm
= cast(int*)malloc(int.sizeof
*linebookmarked
.length
);
2117 if (newbm
!is null) {
2118 scope(exit
) free(newbm
);
2120 bool smthWasChanged
= false;
2121 foreach (int lidx
; linebookmarked
.byKey
) {
2122 // remove "first line" bookmark if "first line" is deleted
2123 if (wholeFirstLineDeleted
&& lidx
== py
) { smthWasChanged
= true; continue; }
2124 // remove "last line" bookmark if "last line" is deleted
2125 if (wholeLastLineDeleted
&& lidx
== ey
) { smthWasChanged
= true; continue; }
2126 // remove bookmarks that are in range
2127 if (lidx
> py
&& lidx
< ey
) continue;
2128 // fix bookmark line if necessary
2129 if (lidx
>= ey
) { smthWasChanged
= true; lidx
-= eolcount
; }
2130 if (lidx
>= 0 && lidx
< lc
.linecount
) {
2131 //assert(lidx >= 0 && lidx < lc.linecount);
2132 // add this bookmark to new list
2133 newbm
[newbmpos
++] = lidx
;
2136 // rebuild list if something was changed
2137 if (smthWasChanged
) {
2138 fullDirty(); //TODO: optimize this
2139 linebookmarked
.clear
;
2140 foreach (int lidx
; newbm
[0..newbmpos
]) linebookmarked
[lidx
] = true;
2143 // out of memory, what to do? just clear bookmarks for now
2144 linebookmarked
.clear
;
2145 fullDirty(); // just in case
2150 /// call this from `willBeInserted()` or `wasInserted()` to fix bookmarks
2151 final void bookmarkInsertionFix (int pos
, int len
, int eolcount
) nothrow {
2152 if (eolcount
&& linebookmarked
.length
> 0) {
2153 import core
.stdc
.stdlib
: malloc
, free
;
2154 // move affected bookmarks down
2155 auto py
= lc
.pos2line(pos
);
2156 if (pos
!= lc
.line2pos(py
)) ++py
; // not the whole first line was modified, don't touch bookmarks on it
2157 // build new bookmark array
2158 int* newbm
= cast(int*)malloc(int.sizeof
*linebookmarked
.length
);
2159 if (newbm
!is null) {
2160 scope(exit
) free(newbm
);
2162 bool smthWasChanged
= false;
2163 foreach (int lidx
; linebookmarked
.byKey
) {
2164 // fix bookmark line if necessary
2165 if (lidx
>= py
) { smthWasChanged
= true; lidx
+= eolcount
; }
2166 if (lidx
< 0 || lidx
>= lc
.linecount
) continue;
2167 //assert(lidx >= 0 && lidx < gb.linecount);
2168 // add this bookmark to new list
2169 newbm
[newbmpos
++] = lidx
;
2171 // rebuild list if something was changed
2172 if (smthWasChanged
) {
2173 fullDirty(); //TODO: optimize this
2174 linebookmarked
.clear
;
2175 foreach (int lidx
; newbm
[0..newbmpos
]) linebookmarked
[lidx
] = true;
2178 // out of memory, what to do? just clear bookmarks for now
2179 linebookmarked
.clear
;
2180 fullDirty(); // just in case
2187 this (int x0
, int y0
, int w
, int h
, EditorHL ahl
=null, bool asingleline
=false) {
2194 //setDirtyLinesLength(visibleLinesPerWindow);
2195 gb
= new GapBuffer(asingleline
);
2196 lc
= new LineCache(gb
);
2197 lc
.recode1byte
= &recode1b
;
2199 if (ahl
!is null) { hl
.gb
= gb
; hl
.lc
= lc
; }
2200 undo
= new UndoStack(mAsRich
, false, !asingleline
);
2201 redo
= new UndoStack(mAsRich
, true, !asingleline
);
2202 mSingleLine
= asingleline
;
2205 private void setDirtyLinesLength (usize len
) nothrow {
2206 if (len
> int.max
/4) assert(0, "wtf?!");
2207 if (dirtyLines
.length
> len
) {
2208 dirtyLines
.length
= len
;
2209 dirtyLines
.assumeSafeAppend
;
2211 } else if (dirtyLines
.length
< len
) {
2212 auto optr
= dirtyLines
.ptr
;
2213 auto olen
= dirtyLines
.length
;
2214 dirtyLines
.length
= len
;
2215 if (dirtyLines
.ptr
!is optr
) {
2216 import core
.memory
: GC
;
2217 if (dirtyLines
.ptr
is GC
.addrOf(dirtyLines
.ptr
)) GC
.setAttr(dirtyLines
.ptr
, GC
.BlkAttr
.NO_INTERIOR
);
2219 //dirtyLines[olen..$] = -1;
2224 // utfuck switch hooks
2225 protected void beforeUtfuckSwitch (bool newisutfuck
) {} /// utfuck switch hook
2226 protected void afterUtfuckSwitch (bool newisutfuck
) {} /// utfuck switch hook
2230 bool utfuck () const pure nothrow @safe @nogc { pragma(inline
, true); return lc
.utfuck
; }
2232 /// this switches "utfuck" mode
2233 /// note that utfuck mode is FUCKIN' SLOW and buggy
2234 /// you should not lose any text, but may encounter visual and positional glitches
2235 void utfuck (bool v
) {
2236 if (lc
.utfuck
== v
) return;
2237 beforeUtfuckSwitch(v
);
2240 lc
.pos2xy(pos
, cx
, cy
);
2242 afterUtfuckSwitch(v
);
2245 ref inout(GapBuffer
.HighState
) defaultRichStyle () inout pure nothrow @trusted @nogc { pragma(inline
, true); return cast(typeof(return))gb
.defhs
; } ///
2247 @property bool asRich () const pure nothrow @safe @nogc { pragma(inline
, true); return mAsRich
; } ///
2249 /// WARNING! changing this will reset undo/redo buffers!
2250 void asRich (bool v
) {
2252 // detach highlighter for "rich mode"
2253 if (v
&& hl
!is null) {
2259 if (undo
!is null) {
2261 undo
= new UndoStack(mAsRich
, false, !singleline
);
2263 if (redo
!is null) {
2265 redo
= new UndoStack(mAsRich
, true, !singleline
);
2267 gb
.hasHiBuffer
= v
; // "rich" mode require highlighting buffer, normal mode doesn't, as it has no highlighter
2268 if (v
&& !gb
.hasHiBuffer
) assert(0, "out of memory"); // alas
2272 @property bool hasHiBuffer () const pure nothrow @safe @nogc { pragma(inline
, true); return gb
.hasHiBuffer
; }
2273 @property void hasHiBuffer (bool v
) nothrow @trusted @nogc {
2274 if (mAsRich
) return; // cannot change
2275 if (hl
!is null) return; // cannot change too
2276 gb
.hasHiBuffer
= v
; // otherwise it is ok to change it
2279 int x0 () const pure nothrow @safe @nogc { pragma(inline
, true); return winx
; } ///
2280 int y0 () const pure nothrow @safe @nogc { pragma(inline
, true); return winy
; } ///
2281 int width () const pure nothrow @safe @nogc { pragma(inline
, true); return winw
; } ///
2282 int height () const pure nothrow @safe @nogc { pragma(inline
, true); return winh
; } ///
2284 void x0 (int v
) { pragma(inline
, true); move(v
, winy
); } ///
2285 void y0 (int v
) { pragma(inline
, true); move(winx
, v
); } ///
2286 void width (int v
) { pragma(inline
, true); resize(v
, winh
); } ///
2287 void height (int v
) { pragma(inline
, true); resize(winw
, v
); } ///
2289 /// has any effect only if you are using `insertText()` and `deleteText()` API!
2290 bool readonly () const pure nothrow @safe @nogc { pragma(inline
, true); return mReadOnly
; }
2291 void readonly (bool v
) nothrow { pragma(inline
, true); mReadOnly
= v
; } ///
2293 /// "single line" mode, for line editors
2294 bool singleline () const pure nothrow @safe @nogc { pragma(inline
, true); return mSingleLine
; }
2296 /// "buffer change counter"
2297 uint bufferCC () const pure nothrow @safe @nogc { pragma(inline
, true); return gb
.bufferChangeCounter
; }
2298 void bufferCC (uint v
) pure nothrow { pragma(inline
, true); gb
.bufferChangeCounter
= v
; } ///
2300 bool killTextOnChar () const pure nothrow @safe @nogc { pragma(inline
, true); return mKillTextOnChar
; } ///
2301 void killTextOnChar (bool v
) nothrow { ///
2302 pragma(inline
, true);
2303 if (mKillTextOnChar
!= v
) {
2304 mKillTextOnChar
= v
;
2309 bool inPixels () const pure nothrow @safe @nogc { pragma(inline
, true); return (lineHeightPixels
!= 0); } ///
2311 /// this can recalc height cache
2312 int linesPerWindow () nothrow {
2313 pragma(inline
, true);
2315 lineHeightPixels
== 0 || lineHeightPixels
== 1 ? winh
:
2316 lineHeightPixels
> 0 ?
(winh
<= lineHeightPixels ?
1 : winh
/lineHeightPixels
) :
2317 calcLinesPerWindow();
2320 /// this can recalc height cache
2321 int visibleLinesPerWindow () nothrow {
2322 pragma(inline
, true);
2324 lineHeightPixels
== 0 || lineHeightPixels
== 1 ? winh
:
2325 lineHeightPixels
> 0 ?
(winh
<= lineHeightPixels ?
1 : winh
/lineHeightPixels
+(winh
%lineHeightPixels ?
1 : 0)) :
2326 calcVisLinesPerWindow();
2330 // for variable line height
2331 protected final int calcVisLinesPerWindow () nothrow {
2332 if (textMeter
is null) assert(0, "you forgot to setup `textMeter` for EditorEngine");
2334 if (hgtleft
< 1) return 1; // just in case
2335 int lidx
= mTopLine
;
2337 while (hgtleft
> 0) {
2338 auto lh
= lc
.lineHeightPixels(lidx
++);
2345 // for variable line height
2346 protected final int calcLinesPerWindow () nothrow {
2347 if (textMeter
is null) assert(0, "you forgot to setup `textMeter` for EditorEngine");
2349 if (hgtleft
< 1) return 1; // just in case
2350 int lidx
= mTopLine
;
2352 //{ import core.stdc.stdio; printf("=== clpw ===\n"); }
2354 auto lh
= lc
.lineHeightPixels(lidx
++);
2355 //if (gb.mLineCount > 0) { import core.stdc.stdio; printf("*clpw: lidx=%d; height=%d; hgtleft=%d\n", lidx-1, lh, hgtleft); }
2357 if (hgtleft
>= 0) ++lcount
;
2358 if (hgtleft
<= 0) break;
2360 //{ import core.stdc.stdio; printf("clpw: %d\n", lcount); }
2361 return (lcount ? lcount
: 1);
2364 /// has lille sense if `inPixels` is false
2365 final int linePixelHeight (int lidx
) nothrow {
2366 if (!inPixels
) return 1;
2367 if (lineHeightPixels
> 0) {
2368 return lineHeightPixels
;
2370 if (textMeter
is null) assert(0, "you forgot to setup `textMeter` for EditorEngine");
2371 return lc
.lineHeightPixels(lidx
);
2376 void resize (int nw
, int nh
) {
2379 if (nw
!= winw || nh
!= winh
) {
2382 auto nvl
= visibleLinesPerWindow
;
2383 setDirtyLinesLength(nvl
);
2384 makeCurLineVisible();
2390 void move (int nx
, int ny
) {
2391 if (winx
!= nx || winy
!= ny
) {
2398 /// move and resize control
2399 void moveResize (int nx
, int ny
, int nw
, int nh
) {
2404 final @property void curx (int v
) nothrow @system { gotoXY(v
, cy
); } ///
2405 final @property void cury (int v
) nothrow @system { gotoXY(cx
, v
); } ///
2407 final @property nothrow {
2408 /// has active marked block?
2409 bool hasMarkedBlock () const pure @safe @nogc { pragma(inline
, true); return (bstart
< bend
); }
2411 int curx () const pure @safe @nogc { pragma(inline
, true); return cx
; } ///
2412 int cury () const pure @safe @nogc { pragma(inline
, true); return cy
; } ///
2413 int xofs () const pure @safe @nogc { pragma(inline
, true); return mXOfs
; } ///
2415 int topline () const pure @safe @nogc { pragma(inline
, true); return mTopLine
; } ///
2416 int linecount () const pure @safe @nogc { pragma(inline
, true); return lc
.linecount
; } ///
2417 int textsize () const pure @safe @nogc { pragma(inline
, true); return gb
.textsize
; } ///
2419 char opIndex (int pos
) const pure @safe @nogc { pragma(inline
, true); return gb
[pos
]; } /// returns '\n' for out-of-bounds query
2421 /// returns '\n' for out-of-bounds query
2422 dchar dcharAt (int pos
) const {
2423 auto ts
= gb
.textsize
;
2424 if (pos
< 0 || pos
>= ts
) return '\n';
2426 final switch (codepage
) {
2427 case CodePage
.koi8u
: return koi2uni(gb
[pos
]);
2428 case CodePage
.cp1251
: return cp12512uni(gb
[pos
]);
2429 case CodePage
.cp866
: return cp8662uni(gb
[pos
]);
2433 Utf8DecoderFast udc
;
2435 if (udc
.decodeSafe(cast(ubyte)gb
[pos
++])) return cast(dchar)udc
.codepoint
;
2437 return udc
.replacement
;
2440 /// this advances `pos`, and returns '\n' for out-of-bounds query
2441 dchar dcharAtAdvance (ref int pos
) const {
2442 auto ts
= gb
.textsize
;
2443 if (pos
< 0) { pos
= 0; return '\n'; }
2444 if (pos
>= ts
) { pos
= ts
; return '\n'; }
2446 immutable char ch
= gb
[pos
++];
2447 final switch (codepage
) {
2448 case CodePage
.koi8u
: return koi2uni(ch
);
2449 case CodePage
.cp1251
: return cp12512uni(ch
);
2450 case CodePage
.cp866
: return cp8662uni(ch
);
2454 Utf8DecoderFast udc
;
2456 if (udc
.decodeSafe(cast(ubyte)gb
[pos
++])) return cast(dchar)udc
.codepoint
;
2458 return udc
.replacement
;
2461 /// this works correctly with utfuck
2462 int nextpos (int pos
) const {
2463 if (pos
< 0) return 0;
2464 immutable ts
= gb
.textsize
;
2465 if (pos
>= ts
) return ts
;
2466 if (!lc
.utfuck
) return pos
+1;
2467 Utf8DecoderFast udc
;
2468 while (pos
< ts
) if (udc
.decodeSafe(cast(ubyte)gb
[pos
++])) break;
2472 /// this sometimes works correctly with utfuck
2473 int prevpos (int pos
) const {
2474 if (pos
<= 0) return 0;
2475 immutable ts
= gb
.textsize
;
2476 if (ts
== 0) return 0;
2477 if (pos
> ts
) pos
= ts
;
2480 while (pos
> 0 && !isValidUtf8Start(cast(ubyte)gb
[pos
])) --pos
;
2485 bool textChanged () const pure { pragma(inline
, true); return txchanged
; } ///
2486 void textChanged (bool v
) pure { pragma(inline
, true); txchanged
= v
; } ///
2488 bool visualtabs () const pure { pragma(inline
, true); return (lc
.visualtabs
&& lc
.tabsize
> 0); } ///
2491 void visualtabs (bool v
) {
2492 if (lc
.visualtabs
!= v
) {
2498 ubyte tabsize () const pure { pragma(inline
, true); return lc
.tabsize
; } ///
2501 void tabsize (ubyte v
) {
2502 if (lc
.tabsize
!= v
) {
2504 if (lc
.visualtabs
) fullDirty();
2508 /// mark whole visible text as dirty
2509 final void fullDirty () nothrow { dirtyLines
[] = -1; }
2513 @property void topline (int v
) nothrow {
2515 if (v
> lc
.linecount
) v
= lc
.linecount
-1;
2516 immutable auto moldtop
= mTopLine
;
2517 mTopLine
= v
; // for linesPerWindow
2518 if (v
+linesPerWindow
> lc
.linecount
) {
2519 v
= lc
.linecount
-linesPerWindow
;
2524 version(egeditor_record_movement_undo
) pushUndoCurPos();
2529 /// absolute coordinates in text
2530 final void gotoXY(bool vcenter
=false) (int nx
, int ny
) nothrow {
2533 if (ny
>= lc
.linecount
) ny
= lc
.linecount
-1;
2534 auto pos
= lc
.xy2pos(nx
, ny
);
2535 lc
.pos2xy(pos
, nx
, ny
);
2536 if (nx
!= cx || ny
!= cy
) {
2537 version(egeditor_record_movement_undo
) pushUndoCurPos();
2540 static if (vcenter
) makeCurLineVisibleCentered(); else makeCurLineVisible();
2545 final void gotoPos(bool vcenter
=false) (int pos
) nothrow {
2546 if (pos
< 0) pos
= 0;
2547 if (pos
> gb
.textsize
) pos
= gb
.textsize
;
2549 lc
.pos2xy(pos
, rx
, ry
);
2550 gotoXY
!vcenter(rx
, ry
);
2553 final int curpos () nothrow { pragma(inline
, true); return lc
.xy2pos(cx
, cy
); } ///
2554 final void curpos (int pos
) nothrow { pragma(inline
, true); gotoPos(pos
); } ///
2556 /// get text coordinates for the given position
2557 void pos2xy (int pos
, out int x
, out int y
) nothrow { pragma(inline
, true); lc
.pos2xy(pos
, out x
, out y
); }
2559 /// get position for the given text coordinates
2560 int xy2pos (int x
, int y
) nothrow { pragma(inline
, true); return lc
.xy2pos(x
, y
); }
2563 void clearUndo () nothrow {
2564 if (undo
!is null) undo
.clear();
2565 if (redo
!is null) redo
.clear();
2569 void clear () nothrow {
2572 if (undo
!is null) undo
.clear();
2573 if (redo
!is null) redo
.clear();
2574 cx
= cy
= mTopLine
= mXOfs
= 0;
2579 markingBlock
= false;
2585 void clearAndDisableUndo () {
2586 if (undo
!is null) delete undo
;
2587 if (redo
!is null) delete redo
;
2591 void reinstantiateUndo () {
2592 if (undo
is null) undo
= new UndoStack(mAsRich
, false, !mSingleLine
);
2593 if (redo
is null) redo
= new UndoStack(mAsRich
, true, !mSingleLine
);
2597 void loadFile (const(char)[] fname
) { loadFile(VFile(fname
)); }
2600 void loadFile (VFile fl
) {
2601 import core
.stdc
.stdlib
: malloc
, free
;
2603 scope(failure
) clear();
2604 auto fpos
= fl
.tell
;
2607 if (fsz
-fpos
>= gb
.tbmax
) throw new Exception("text too big");
2608 uint filesize
= cast(uint)(fsz
-fpos
);
2609 if (!lc
.resizeBuffer(filesize
)) throw new Exception("text too big");
2610 scope(failure
) clear();
2611 fl
.rawReadExact(lc
.getBufferPtr
[]);
2612 if (!lc
.rebuild()) throw new Exception("out of memory");
2617 void saveFile (const(char)[] fname
) { saveFile(VFile(fname
, "w")); }
2620 void saveFile (VFile fl
) {
2621 gb
.forEachBufPart(0, gb
.textsize
, delegate (const(char)[] buf
) { fl
.rawWriteExact(buf
); });
2623 if (undo
!is null) undo
.alwaysChanged();
2624 if (redo
!is null) redo
.alwaysChanged();
2627 /// attach new highlighter; return previous one
2628 /// note that you can't reuse one highlighter for several editors!
2629 EditorHL
attachHiglighter (EditorHL ahl
) {
2630 if (mAsRich
) { assert(hl
is null); return null; } // oops
2631 if (ahl
is hl
) return ahl
; // nothing to do
2632 EditorHL prevhl
= hl
;
2639 gb
.hasHiBuffer
= false; // don't need it
2642 return prevhl
; // return previous
2644 if (ahl
.lc
!is null) {
2645 if (ahl
.lc
!is lc
) throw new Exception("highlighter already used by another editor");
2646 if (ahl
!is hl
) assert(0, "something is VERY wrong");
2649 if (hl
!is null) { hl
.gb
= null; hl
.lc
= null; }
2653 gb
.hasHiBuffer
= true; // need it
2654 if (!gb
.hasHiBuffer
) assert(0, "out of memory"); // alas
2655 ahl
.lineChanged(0, true);
2661 EditorHL
detachHighlighter () {
2662 if (mAsRich
) { assert(hl
is null); return null; } // oops
2668 gb
.hasHiBuffer
= false; // don't need it
2674 /// override this method to draw something before any other page drawing will be done
2675 public void drawPageBegin () {}
2677 /// override this method to draw one text line
2678 /// highlighting is done, other housekeeping is done, only draw
2679 /// lidx is always valid
2680 /// must repaint the whole line
2681 /// use `winXXX` vars to know window dimensions
2682 public abstract void drawLine (int lidx
, int yofs
, int xskip
);
2684 /// just clear the line; you have to override this, 'cause it is used to clear empty space
2685 /// use `winXXX` vars to know window dimensions
2686 public abstract void drawEmptyLine (int yofs
);
2688 /// override this method to draw something after page was drawn, but before drawing the status
2689 public void drawPageMisc () {}
2691 /// override this method to draw status line; it will be called after `drawPageBegin()`
2692 public void drawStatus () {}
2694 /// override this method to draw something after status was drawn, but before drawing the cursor
2695 public void drawPagePost () {}
2697 /// override this method to draw text cursor; it will be called after `drawPageMisc()`
2698 public abstract void drawCursor ();
2700 /// override this method to draw something (or flush drawing buffer) after everything was drawn
2701 public void drawPageEnd () {}
2703 /** draw the page; it will fix coords, call necessary methods and so on. you are usually don't need to override this.
2704 * page drawing flow:
2706 * page itself with drawLine() or drawEmptyLine();
2714 makeCurLineVisible();
2716 if (prevTopLine
!= mTopLine || prevXOfs
!= mXOfs
) {
2717 prevTopLine
= mTopLine
;
2723 immutable int lhp
= lineHeightPixels
;
2724 immutable int ydelta
= (inPixels ? lhp
: 1);
2725 bool alwaysDirty
= false;
2726 auto pos
= lc
.xy2pos(0, mTopLine
);
2727 auto lc
= lc
.linecount
;
2729 //TODO: optimize redrawing for variable line height mode
2730 foreach (int y
; 0..visibleLinesPerWindow
) {
2731 bool dirty
= (mTopLine
+y
< lc
&& hl
!is null && hl
.fixLine(mTopLine
+y
));
2734 // variable line height, hacks
2735 alwaysDirty
= (!alwaysDirty
&& y
< dirtyLines
.length ?
(dirtyLines
.ptr
[y
] != linePixelHeight(mTopLine
+y
)) : true);
2736 } else if (!dirty
&& y
< dirtyLines
.length
) {
2737 // tty or constant pixel height
2738 dirty
= (dirtyLines
.ptr
[y
] != 0);
2742 if (dirty || alwaysDirty
) {
2743 if (y
< dirtyLines
.length
) dirtyLines
.ptr
[y
] = (lhp
>= 0 ?
0 : linePixelHeight(mTopLine
+y
));
2744 if (mTopLine
+y
< lc
) {
2745 drawLine(mTopLine
+y
, lyofs
, mXOfs
);
2747 drawEmptyLine(lyofs
);
2750 lyofs
+= (ydelta
> 0 ? ydelta
: linePixelHeight(mTopLine
+y
));
2759 /// force cursor coordinates to be in text
2760 final void normXY () nothrow {
2761 lc
.pos2xy(curpos
, cx
, cy
);
2765 final void makeCurXVisible () nothrow {
2766 // use "real" x coordinate to calculate x offset
2771 lc
.pos2xyVT(curpos
, rx
, ry
);
2772 if (rx
< mXOfs
) mXOfs
= rx
;
2773 if (rx
-mXOfs
>= winw
) mXOfs
= rx
-winw
+1;
2775 rx
= localCursorX();
2777 if (rx
< mXOfs
) mXOfs
= rx
-8;
2778 if (rx
+4-mXOfs
> winw
) mXOfs
= rx
-winw
+4;
2780 if (mXOfs
< 0) mXOfs
= 0;
2783 /// in symbols, not chars
2784 final int linelen (int lidx
) nothrow {
2785 if (lidx
< 0 || lidx
>= lc
.linecount
) return 0;
2786 auto pos
= lc
.line2pos(lidx
);
2787 auto ts
= gb
.textsize
;
2788 if (pos
> ts
) pos
= ts
;
2791 if (mSingleLine
) return ts
-pos
;
2793 if (gb
[pos
++] == '\n') break;
2797 immutable bool sl
= mSingleLine
;
2799 char ch
= gb
[pos
++];
2800 if (!sl
&& ch
== '\n') break;
2804 pos
+= gb
.utfuckLenAt(pos
);
2811 /// cursor position in "local" coords: from widget (x0,y0), possibly in pixels
2812 final int localCursorX () nothrow {
2814 localCursorXY(&rx
, null);
2818 /// cursor position in "local" coords: from widget (x0,y0), possibly in pixels
2819 final void localCursorXY (int* lcx
, int* lcy
) nothrow {
2822 lc
.pos2xyVT(curpos
, rx
, ry
);
2825 if (lcx
!is null) *lcx
= rx
;
2826 if (lcy
!is null) *lcy
= ry
;
2828 lc
.pos2xy(curpos
, rx
, ry
);
2829 if (textMeter
is null) assert(0, "you forgot to setup `textMeter` for EditorEngine");
2831 if (lineHeightPixels
> 0) {
2832 *lcy
= (ry
-mTopLine
)*lineHeightPixels
;
2834 if (ry
>= mTopLine
) {
2835 for (int ll
= mTopLine
; ll
< ry
; ++ll
) *lcy
+= lc
.lineHeightPixels(ll
);
2837 for (int ll
= mTopLine
-1; ll
>= ry
; --ll
) *lcy
-= lc
.lineHeightPixels(ll
);
2841 if (rx
== 0) { if (lcx
!is null) *lcx
= 0-mXOfs
; return; }
2843 textMeter
.reset(visualtabs ? lc
.tabsize
: 0);
2844 scope(exit
) textMeter
.finish(); // just in case
2845 auto pos
= lc
.linestart(ry
);
2846 immutable int le
= lc
.linestart(ry
+1);
2847 immutable bool ufuck
= lc
.utfuck
;
2850 // advance one symbol
2851 textMeter
.advance(dcharAtAdvance(pos
), gb
.hi(pos
));
2857 // advance one symbol
2858 if (gb
[pos
] == '\n') break;
2859 textMeter
.advance(dcharAtAdvance(pos
), gb
.hi(pos
));
2863 if (gb
[pos
] != '\n' && pos
< le
) {
2864 textMeter
.advance(dcharAtAdvance(pos
), gb
.hi(pos
));
2865 *lcx
= textMeter
.currofs
;
2872 *lcx
= textMeter
.currwdt
-mXOfs
;
2877 /// convert coordinates in widget into text coordinates; can be used to convert mouse click position into text position
2878 /// WARNING: ty can be equal to linecount or -1!
2879 final void widget2text (int mx
, int my
, out int tx
, out int ty
) nothrow {
2881 int ry
= my
+mTopLine
;
2882 if (ry
< 0) { ty
= -1; return; } // tx is zero here
2883 if (ry
>= lc
.linecount
) { ty
= lc
.linecount
; return; } // tx is zero here
2884 if (mx
<= 0 && mXOfs
== 0) return; // tx is zero here
2885 // ah, screw it! user should not call this very often, so i can stop caring about speed.
2887 auto pos
= lc
.line2pos(ry
);
2888 auto ts
= gb
.textsize
;
2890 immutable bool ufuck
= lc
.utfuck
;
2891 immutable bool sl
= mSingleLine
;
2893 // advance one symbol
2895 if (!sl
&& ch
== '\n') { tx
= rx
; return; } // done anyway
2897 if (ch
== '\t' && visualtabs
) {
2899 nextx
= ((visx
+mXOfs
)/tabsize
+1)*tabsize
-mXOfs
;
2901 if (mx
>= visx
&& mx
< nextx
) { tx
= rx
; return; }
2903 if (!ufuck || ch
< 128) {
2911 if (textMeter
is null) assert(0, "you forgot to setup `textMeter` for EditorEngine");
2913 if (lineHeightPixels
> 0) {
2914 ry
= my
/lineHeightPixels
+mTopLine
;
2921 lcy
+= lc
.lineHeightPixels(ry
);
2922 if (lcy
> my
) break;
2924 if (lcy
== my
) break;
2931 int upy
= lcy
-lc
.lineHeightPixels(ry
);
2932 if (my
>= upy
&& my
< lcy
) break;
2937 if (ry
< 0) { ty
= -1; return; } // tx is zero here
2938 if (ry
>= lc
.linecount
) { ty
= lc
.linecount
; return; } // tx is zero here
2940 if (mx
<= 0 && mXOfs
== 0) return; // tx is zero here
2941 // now the hard part
2942 textMeter
.reset(visualtabs ? lc
.tabsize
: 0);
2943 scope(exit
) textMeter
.finish(); // just in case
2945 auto pos
= lc
.line2pos(ry
);
2946 immutable ts
= gb
.textsize
;
2948 immutable bool ufuck
= lc
.utfuck
;
2949 immutable bool sl
= mSingleLine
;
2951 // advance one symbol
2953 if (!sl
&& ch
== '\n') { tx
= rx
; return; } // done anyway
2954 if (!ufuck || ch
< 128) {
2955 textMeter
.advance(cast(dchar)ch
, gb
.hi(pos
));
2958 textMeter
.advance(dcharAtAdvance(pos
), gb
.hi(pos
));
2960 immutable int visx1
= textMeter
.currwdt
-mXOfs
;
2961 // visx0 is current char x start
2962 // visx1 is current char x end
2963 // so if our mx is in [visx0..visx1), we are at current char
2964 if (mx
>= visx0
&& mx
< visx1
) {
2965 // it is more natural this way
2966 if (mx
>= visx0
+(visx1
-visx0
)/2 && pos
< lc
.linestart(ry
+1)) ++rx
;
2977 final void makeCurLineVisible () nothrow {
2979 if (cy
>= lc
.linecount
) cy
= lc
.linecount
-1;
2980 if (cy
< mTopLine
) {
2983 if (cy
> mTopLine
+linesPerWindow
-1) {
2984 mTopLine
= cy
-linesPerWindow
+1;
2985 if (mTopLine
< 0) mTopLine
= 0;
2988 setDirtyLinesLength(visibleLinesPerWindow
);
2993 final void makeCurLineVisibleCentered (bool forced
=false) nothrow {
2994 if (forced ||
!isCurLineVisible
) {
2996 if (cy
>= lc
.linecount
) cy
= lc
.linecount
-1;
2997 mTopLine
= cy
-linesPerWindow
/2;
2998 if (mTopLine
< 0) mTopLine
= 0;
2999 if (mTopLine
+linesPerWindow
> lc
.linecount
) {
3000 mTopLine
= lc
.linecount
-linesPerWindow
;
3001 if (mTopLine
< 0) mTopLine
= 0;
3004 setDirtyLinesLength(visibleLinesPerWindow
);
3009 final bool isCurLineBeforeTop () nothrow {
3010 pragma(inline
, true);
3011 return (cy
< mTopLine
);
3015 final bool isCurLineAfterBottom () nothrow {
3016 pragma(inline
, true);
3017 return (cy
> mTopLine
+linesPerWindow
-1);
3021 final bool isCurLineVisible () nothrow {
3022 pragma(inline
, true);
3023 return (cy
>= mTopLine
&& cy
< mTopLine
+linesPerWindow
);
3024 //if (cy < mTopLine) return false;
3025 //if (cy > mTopLine+linesPerWindow-1) return false;
3029 /// `updateDown`: update all the page (as new lines was inserted/removed)
3030 final void lineChanged (int lidx
, bool updateDown
) {
3031 if (lidx
< 0 || lidx
>= lc
.linecount
) return;
3032 if (hl
!is null) hl
.lineChanged(lidx
, updateDown
);
3033 if (lidx
< mTopLine
) { if (updateDown
) dirtyLines
[] = -1; return; }
3034 if (lidx
>= mTopLine
+linesPerWindow
) return;
3035 immutable stl
= lidx
-mTopLine
;
3037 if (stl
< dirtyLines
.length
) {
3039 dirtyLines
[stl
..$] = -1;
3041 dirtyLines
.ptr
[stl
] = -1;
3047 final void lineChangedByPos (int pos
, bool updateDown
) { return lineChanged(lc
.pos2line(pos
), updateDown
); }
3050 final void markLinesDirty (int lidx
, int count
) nothrow {
3051 if (prevTopLine
!= mTopLine || prevXOfs
!= mXOfs
) return; // we will refresh the whole page anyway
3052 if (count
< 1 || lidx
>= lc
.linecount
) return;
3053 if (count
> lc
.linecount
) count
= lc
.linecount
;
3054 if (lidx
>= mTopLine
+linesPerWindow
) return;
3055 int le
= lidx
+count
;
3056 if (le
<= mTopLine
) { dirtyLines
[] = -1; return; } // just in case
3057 if (lidx
< mTopLine
) { dirtyLines
[] = -1; lidx
= mTopLine
; return; } // just in cale
3058 if (le
> mTopLine
+visibleLinesPerWindow
) le
= mTopLine
+visibleLinesPerWindow
;
3059 immutable stl
= lidx
-mTopLine
;
3061 if (stl
< dirtyLines
.length
) {
3062 auto el
= le
-mTopLine
;
3063 if (el
> dirtyLines
.length
) el
= cast(int)dirtyLines
.length
;
3064 dirtyLines
.ptr
[stl
..el
] = -1;
3069 final void markLinesDirtySE (int lidxs
, int lidxe
) nothrow {
3070 if (lidxe
< lidxs
) { int tmp
= lidxs
; lidxs
= lidxe
; lidxe
= tmp
; }
3071 markLinesDirty(lidxs
, lidxe
-lidxs
+1);
3075 final void markRangeDirty (int pos
, int len
) nothrow {
3076 if (prevTopLine
!= mTopLine || prevXOfs
!= mXOfs
) return; // we will refresh the whole page anyway
3077 int l0
= lc
.pos2line(pos
);
3078 int l1
= lc
.pos2line(pos
+len
+1);
3079 markLinesDirtySE(l0
, l1
);
3083 final void markBlockDirty () nothrow {
3084 //FIXME: optimize updating with block boundaries
3085 if (bstart
>= bend
) return;
3086 markRangeDirty(bstart
, bend
-bstart
);
3089 /// do various fixups before text deletion
3090 /// cursor coords *may* be already changed
3091 /// will be called before text deletion by `deleteText` or `replaceText` APIs
3092 /// eolcount: number of eols in (to be) deleted block
3093 protected void willBeDeleted (int pos
, int len
, int eolcount
) nothrow {
3094 //FIXME: optimize updating with block boundaries
3095 if (len
< 1) return; // just in case
3096 assert(pos
>= 0 && cast(long)pos
+len
<= gb
.textsize
);
3097 bookmarkDeletionFix(pos
, len
, eolcount
);
3098 if (hasMarkedBlock
) {
3099 if (pos
+len
<= bstart
) {
3100 // move whole block up
3106 } else if (pos
<= bstart
&& pos
+len
>= bend
) {
3107 // whole block will be deleted
3108 doBlockResetMark(false); // skip undo
3109 } else if (pos
>= bstart
&& pos
+len
<= bend
) {
3110 // deleting something inside block, move end
3113 if (bstart
>= bend
) {
3114 doBlockResetMark(false); // skip undo
3119 } else if (pos
>= bstart
&& pos
< bend
&& pos
+len
> bend
) {
3120 // chopping block end
3123 if (bstart
>= bend
) {
3124 doBlockResetMark(false); // skip undo
3133 /// do various fixups after text deletion
3134 /// cursor coords *may* be already changed
3135 /// will be called after text deletion by `deleteText` or `replaceText` APIs
3136 /// eolcount: number of eols in deleted block
3137 /// pos and len: they were valid *before* deletion!
3138 protected void wasDeleted (int pos
, int len
, int eolcount
) nothrow {
3141 /// do various fixups before text insertion
3142 /// cursor coords *may* be already changed
3143 /// will be called before text insertion by `insertText` or `replaceText` APIs
3144 /// eolcount: number of eols in (to be) inserted block
3145 protected void willBeInserted (int pos
, int len
, int eolcount
) nothrow {
3148 /// do various fixups after text insertion
3149 /// cursor coords *may* be already changed
3150 /// will be called after text insertion by `insertText` or `replaceText` APIs
3151 /// eolcount: number of eols in inserted block
3152 protected void wasInserted (int pos
, int len
, int eolcount
) nothrow {
3153 //FIXME: optimize updating with block boundaries
3154 if (len
< 1) return;
3155 assert(pos
>= 0 && cast(long)pos
+len
<= gb
.textsize
);
3156 bookmarkInsertionFix(pos
, len
, eolcount
);
3157 if (markingBlock
&& pos
== bend
) {
3163 if (hasMarkedBlock
) {
3164 if (pos
<= bstart
) {
3165 // move whole block down
3171 } else if (pos
< bend
) {
3172 // move end of block down
3181 /// should be called after cursor position change
3182 protected final void growBlockMark () nothrow {
3183 if (!markingBlock || bstart
< 0) return;
3184 makeCurLineVisible();
3190 ry
= lc
.pos2line(bend
);
3196 ry
= lc
.pos2line(bstart
);
3197 if (bstart
== pos
) return;
3201 } else if (pos
> bend
) {
3203 if (bend
== pos
) return;
3204 ry
= lc
.pos2line(bend
-1);
3207 } else if (pos
>= bstart
&& pos
< bend
) {
3211 if (bend
== pos
) return;
3212 ry
= lc
.pos2line(bend
-1);
3216 if (bstart
== pos
) return;
3217 ry
= lc
.pos2line(bstart
);
3221 markLinesDirtySE(ry
, cy
);
3224 /// all the following text operations will be grouped into one undo action
3225 bool undoGroupStart () {
3226 return (undo
!is null ? undo
.addGroupStart(this) : false);
3229 /// end undo action started with `undoGroupStart()`
3230 bool undoGroupEnd () {
3231 return (undo
!is null ? undo
.addGroupEnd(this) : false);
3234 /// build autoindent for the current line, put it into `indentText`
3235 /// `indentText` will include '\n'
3236 protected final void buildIndent (int pos
) {
3237 if (indentText
.length
) { indentText
.length
= 0; indentText
.assumeSafeAppend
; }
3238 void putToIT (char ch
) {
3239 auto optr
= indentText
.ptr
;
3241 if (optr
!is indentText
.ptr
) {
3242 import core
.memory
: GC
;
3243 if (indentText
.ptr
is GC
.addrOf(indentText
.ptr
)) {
3244 GC
.setAttr(indentText
.ptr
, GC
.BlkAttr
.NO_INTERIOR
); // less false positives
3249 pos
= lc
.line2pos(lc
.pos2line(pos
));
3250 auto ts
= gb
.textsize
;
3253 if (curx
== cx
) break;
3255 if (ch
== '\n') break;
3256 if (ch
> ' ') break;
3263 /// delete text, save undo, mark updated lines
3264 /// return `false` if operation cannot be performed
3265 /// if caller wants to delete more text than buffer has, it is ok
3266 /// calls `dg` *after* undo saving, but before `willBeDeleted()`
3267 final bool deleteText(string movecursor
="none") (int pos
, int count
, scope void delegate (int pos
, int count
) dg
=null) {
3268 static assert(movecursor
== "none" || movecursor
== "start" || movecursor
== "end");
3269 if (mReadOnly
) return false;
3270 killTextOnChar
= false;
3271 auto ts
= gb
.textsize
;
3272 if (pos
< 0 || pos
>= ts || count
< 0) return false;
3273 if (ts
-pos
< count
) count
= ts
-pos
;
3275 bool undoOk
= false;
3276 if (undo
!is null) undoOk
= undo
.addTextRemove(this, pos
, count
);
3277 if (dg
!is null) dg(pos
, count
);
3278 int delEols
= (!mSingleLine ? gb
.countEolsInRange(pos
, count
) : 0);
3279 willBeDeleted(pos
, count
, delEols
);
3280 //writeLogAction(pos, -count);
3281 // hack: if new linecount is different, there was '\n' in text
3282 auto olc
= lc
.linecount
;
3283 if (!lc
.remove(pos
, count
)) {
3284 if (undoOk
) undo
.popUndo(); // remove undo record
3288 static if (movecursor
!= "none") lc
.pos2xy(pos
, cx
, cy
);
3289 wasDeleted(pos
, count
, delEols
);
3290 lineChangedByPos(pos
, (lc
.linecount
!= olc
));
3292 static if (movecursor
!= "none") {
3294 lc
.pos2xy(curpos
, rx
, ry
);
3295 if (rx
!= cx || ry
!= cy
) {
3296 version(egeditor_record_movement_undo
) {
3297 immutable bool okmove
= pushUndoCurPos();
3304 markLinesDirty(cy
, 1);
3312 /// ugly name is intentional
3313 /// this replaces editor text, clears undo and sets `killTextOnChar` if necessary
3314 /// it also ignores "readonly" flag
3315 final bool setNewText (const(char)[] text
, bool killOnChar
=true) {
3316 auto oldro
= mReadOnly
;
3317 scope(exit
) mReadOnly
= oldro
;
3320 auto res
= insertText
!"end"(0, text
);
3322 if (mSingleLine
) killTextOnChar
= killOnChar
;
3327 /// insert text, save undo, mark updated lines
3328 /// return `false` if operation cannot be performed
3329 final bool insertText(string movecursor
="none", bool doIndent
=true) (int pos
, const(char)[] str) {
3330 static assert(movecursor
== "none" || movecursor
== "start" || movecursor
== "end");
3331 if (mReadOnly
) return false;
3332 if (mKillTextOnChar
) {
3333 killTextOnChar
= false;
3334 if (gb
.textsize
> 0) {
3337 markingBlock
= false;
3338 deleteText
!"start"(0, gb
.textsize
);
3342 auto ts
= gb
.textsize
;
3343 if (pos
< 0 ||
str.length
>= int.max
/3) return false;
3344 if (pos
> ts
) pos
= ts
;
3345 if (str.length
> 0) {
3346 int nlc
= (!mSingleLine ? GapBuffer
.countEols(str) : 0);
3347 static if (doIndent
) {
3349 // want indenting and has at least one newline, hard case
3351 if (indentText
.length
) {
3352 int toinsert
= cast(int)str.length
+nlc
*(cast(int)indentText
.length
-1);
3353 bool undoOk
= false;
3354 bool doRollback
= false;
3356 if (undo
!is null) undoOk
= undo
.addTextInsert(this, pos
, toinsert
);
3357 willBeInserted(pos
, toinsert
, nlc
);
3360 while (str.length
> 0) {
3361 int elp
= GapBuffer
.findEol(str);
3362 if (elp
< 0) elp
= cast(int)str.length
;
3365 auto newpos
= lc
.put(ipos
, str[0..elp
]);
3366 if (newpos
< 0) { doRollback
= true; break; }
3371 assert(str[0] == '\n');
3372 auto newpos
= lc
.put(ipos
, indentText
);
3373 if (newpos
< 0) { doRollback
= true; break; }
3379 // operation failed, rollback it
3380 if (ipos
> spos
) lc
.remove(spos
, ipos
-spos
); // remove inserted text
3381 if (undoOk
) undo
.popUndo(); // remove undo record
3384 //if (ipos-spos != toinsert) { import core.stdc.stdio : stderr, fprintf; fprintf(stderr, "spos=%d; ipos=%d; ipos-spos=%d; toinsert=%d; nlc=%d; sl=%d; il=%d\n", spos, ipos, ipos-spos, toinsert, nlc, cast(int)str.length, cast(int)indentText.length); }
3385 assert(ipos
-spos
== toinsert
);
3386 static if (movecursor
== "start") lc
.pos2xy(spos
, cx
, cy
);
3387 else static if (movecursor
== "end") lc
.pos2xy(ipos
, cx
, cy
);
3389 lineChangedByPos(spos
, true);
3390 wasInserted(spos
, toinsert
, nlc
);
3395 // either we don't want indenting, or there are no eols in new text
3397 bool undoOk
= false;
3399 if (undo
!is null) undoOk
= undo
.addTextInsert(this, pos
, cast(int)str.length
);
3400 willBeInserted(pos
, cast(int)str.length
, nlc
);
3402 auto newpos
= lc
.put(pos
, str[]);
3404 // operation failed, rollback it
3405 if (undoOk
) undo
.popUndo(); // remove undo record
3408 static if (movecursor
== "start") lc
.pos2xy(pos
, cx
, cy
);
3409 else static if (movecursor
== "end") lc
.pos2xy(newpos
, cx
, cy
);
3411 lineChangedByPos(pos
, (nlc
> 0));
3412 wasInserted(pos
, newpos
-pos
, nlc
);
3418 /// replace text at pos, save undo, mark updated lines
3419 /// return `false` if operation cannot be performed
3420 final bool replaceText(string movecursor
="none", bool doIndent
=false) (int pos
, int count
, const(char)[] str) {
3421 static assert(movecursor
== "none" || movecursor
== "start" || movecursor
== "end");
3422 if (mReadOnly
) return false;
3423 if (count
< 0 || pos
< 0) return false;
3424 if (mKillTextOnChar
) {
3425 killTextOnChar
= false;
3426 if (gb
.textsize
> 0) {
3429 markingBlock
= false;
3430 deleteText
!"start"(0, gb
.textsize
);
3434 auto ts
= gb
.textsize
;
3435 if (pos
>= ts
) pos
= ts
;
3436 if (count
> ts
-pos
) count
= ts
-pos
;
3437 bool needToRestoreBlock
= (markingBlock || hasMarkedBlock
);
3440 auto mb
= markingBlock
;
3442 scope(exit
) undoGroupEnd();
3444 deleteText
!movecursor(pos
, count
);
3445 static if (movecursor
== "none") { bool cmoved
= false; if (ocp
> pos
) { cmoved
= true; ocp
-= count
; } }
3446 if (insertText
!(movecursor
, doIndent
)(pos
, str)) {
3447 static if (movecursor
== "none") { if (cmoved
) ocp
+= count
; }
3448 if (needToRestoreBlock
&& !hasMarkedBlock
) {
3449 // restore block if it was deleted
3451 bend
= be
-count
+cast(int)str.length
;
3453 if (bend
< bstart
) markingBlock
= false;
3455 } else if (hasMarkedBlock
&& bs
== pos
&& bstart
> pos
) {
3456 // consider the case when replaced text is inside the block,
3457 // and block is starting on the text
3459 lastBGEnd
= false; //???
3468 bool doBlockWrite (const(char)[] fname
) {
3469 killTextOnChar
= false;
3470 if (!hasMarkedBlock
) return true;
3471 if (bend
-bstart
<= 0) return true;
3472 return doBlockWrite(VFile(fname
, "w"));
3476 bool doBlockWrite (VFile fl
) {
3477 import core
.stdc
.stdlib
: malloc
, free
;
3478 killTextOnChar
= false;
3479 if (!hasMarkedBlock
) return true;
3480 gb
.forEachBufPart(bstart
, bend
-bstart
, delegate (const(char)[] buf
) { fl
.rawWriteExact(buf
); });
3485 bool doBlockRead (const(char)[] fname
) { return doBlockRead(VFile(fname
)); }
3488 bool doBlockRead (VFile fl
) {
3489 //FIXME: optimize this!
3490 import core
.stdc
.stdlib
: realloc
, free
;
3491 import core
.stdc
.string
: memcpy
;
3492 // read block data into temp buffer
3493 if (mReadOnly
) return false;
3494 killTextOnChar
= false;
3496 scope(exit
) if (btext
!is null) free(btext
);
3498 char[1024] tb
= void;
3500 auto rd
= fl
.rawRead(tb
[]);
3501 if (rd
.length
== 0) break;
3502 if (blen
+rd
.length
> int.max
/2) return false;
3503 auto nb
= cast(char*)realloc(btext
, blen
+rd
.length
);
3504 if (nb
is null) return false;
3506 memcpy(btext
+blen
, rd
.ptr
, rd
.length
);
3507 blen
+= cast(int)rd
.length
;
3509 return insertText
!("start", false)(curpos
, btext
[0..blen
]); // no indent
3513 bool doBlockDelete () {
3514 if (mReadOnly
) return false;
3515 if (!hasMarkedBlock
) return true;
3516 return deleteText
!"start"(bstart
, bend
-bstart
, (pos
, count
) { doBlockResetMark(false); });
3520 bool doBlockCopy () {
3521 //FIXME: optimize this!
3522 import core
.stdc
.stdlib
: malloc
, free
;
3523 if (mReadOnly
) return false;
3524 killTextOnChar
= false;
3525 if (!hasMarkedBlock
) return true;
3526 // copy block data into temp buffer
3527 int blen
= bend
-bstart
;
3528 GapBuffer
.HighState
* hsbuf
;
3529 scope(exit
) if (hsbuf
!is null) free(hsbuf
);
3531 // rich text: get atts
3532 hsbuf
= cast(GapBuffer
.HighState
*)malloc(blen
*hsbuf
[0].sizeof
);
3533 if (hsbuf
is null) return false;
3534 foreach (int pp
; bstart
..bend
) hsbuf
[pp
-bstart
] = gb
.hi(pp
);
3537 char* btext
= cast(char*)malloc(blen
);
3538 if (btext
is null) return false; // alas
3539 scope(exit
) free(btext
);
3540 foreach (int pp
; bstart
..bend
) btext
[pp
-bstart
] = gb
[pp
];
3542 return insertText
!("start", false)(stp
, btext
[0..blen
]); // no indent
3545 foreach (immutable int idx
; 0..blen
) gb
.hi(stp
+idx
) = hsbuf
[idx
];
3550 bool doBlockMove () {
3551 //FIXME: optimize this!
3552 import core
.stdc
.stdlib
: malloc
, free
;
3553 if (mReadOnly
) return false;
3554 killTextOnChar
= false;
3555 if (!hasMarkedBlock
) return true;
3557 if (pos
>= bstart
&& pos
< bend
) return false; // can't do this while we are inside the block
3558 // copy block data into temp buffer
3559 int blen
= bend
-bstart
;
3560 GapBuffer
.HighState
* hsbuf
;
3561 scope(exit
) if (hsbuf
!is null) free(hsbuf
);
3563 // rich text: get atts
3564 hsbuf
= cast(GapBuffer
.HighState
*)malloc(blen
*hsbuf
[0].sizeof
);
3565 if (hsbuf
is null) return false;
3566 foreach (int pp
; bstart
..bend
) hsbuf
[pp
-bstart
] = gb
.hi(pp
);
3568 char* btext
= cast(char*)malloc(blen
);
3569 if (btext
is null) return false; // alas
3570 scope(exit
) free(btext
);
3571 foreach (int pp
; bstart
..bend
) btext
[pp
-bstart
] = gb
[pp
];
3572 // group undo action
3573 bool undoOk
= undoGroupStart();
3574 if (pos
>= bstart
) pos
-= blen
;
3575 if (!doBlockDelete()) {
3577 if (undoOk
) undo
.popUndo();
3581 if (!insertText
!("start", false)(pos
, btext
[0..blen
])) {
3583 if (undoOk
) undo
.popUndo();
3588 foreach (immutable int idx
; 0..blen
) gb
.hi(stp
+idx
) = hsbuf
[idx
];
3600 if (mReadOnly
) return;
3602 if (pos
>= gb
.textsize
) return;
3604 deleteText
!"start"(pos
, 1);
3606 deleteText
!"start"(pos
, gb
.utfuckLenAt(pos
));
3611 void doBackspace () {
3612 if (mReadOnly
) return;
3613 killTextOnChar
= false;
3615 if (pos
== 0) return;
3616 immutable int ppos
= prevpos(pos
);
3617 deleteText
!"start"(ppos
, pos
-ppos
);
3621 void doBackByIndent () {
3622 if (mReadOnly
) return;
3624 int ls
= lc
.xy2pos(0, cy
);
3625 if (pos
== ls
) { doDeleteWord(); return; }
3626 if (gb
[pos
-1] > ' ') { doDeleteWord(); return; }
3628 lc
.pos2xy(pos
, rx
, ry
);
3630 if (del
> 1 && (pos
-2 < ls || gb
[pos
-2] > ' ')) del
= 1;
3632 deleteText
!"start"(pos
, del
);
3636 void doDeleteWord () {
3637 if (mReadOnly
) return;
3639 if (pos
== 0) return;
3640 auto ch
= gb
[pos
-1];
3641 if (!mSingleLine
&& ch
== '\n') { doBackspace(); return; }
3647 if ((!mSingleLine
&& ch
== '\n') || ch
> ' ') break;
3650 } else if (isWordChar(ch
)) {
3653 if (!isWordChar(ch
)) break;
3657 if (pos
== stpos
) return;
3658 deleteText
!"start"(stpos
, pos
-stpos
);
3662 void doKillLine () {
3663 if (mReadOnly
) return;
3664 int ls
= lc
.xy2pos(0, cy
);
3666 if (cy
== lc
.linecount
-1) {
3669 le
= lc
.xy2pos(0, cy
+1);
3671 if (ls
< le
) deleteText
!"start"(ls
, le
-ls
);
3675 void doKillToEOL () {
3676 if (mReadOnly
) return;
3678 auto ts
= gb
.textsize
;
3680 if (pos
< ts
) deleteText
!"start"(pos
, ts
-pos
);
3682 if (pos
< ts
&& gb
[pos
] != '\n') {
3684 while (epos
< ts
&& gb
[epos
] != '\n') ++epos
;
3685 deleteText
!"start"(pos
, epos
-pos
);
3690 /// split line at current position
3691 bool doLineSplit (bool autoindent
=true) {
3692 if (mReadOnly || mSingleLine
) return false;
3694 return insertText
!("end", true)(curpos
, "\n");
3696 return insertText
!("end", false)(curpos
, "\n");
3700 /// put char in koi8
3701 void doPutChar (char ch
) {
3702 if (mReadOnly
) return;
3703 if (!mSingleLine
&& ch
== '\n') { doLineSplit(inPasteMode
<= 0); return; }
3704 if (ch
> 127 && lc
.utfuck
) {
3705 char[8] ubuf
= void;
3706 int len
= utf8Encode(ubuf
[], koi2uni(ch
));
3707 if (len
< 1) { ubuf
[0] = '?'; len
= 1; }
3708 insertText
!("end", true)(curpos
, ubuf
[0..len
]);
3711 if (ch
>= 128 && codepage
!= CodePage
.koi8u
) ch
= recodeCharTo(ch
);
3712 if (inPasteMode
<= 0) {
3713 insertText
!("end", true)(curpos
, (&ch
)[0..1]);
3715 insertText
!("end", false)(curpos
, (&ch
)[0..1]);
3720 void doPutDChar (dchar dch
) {
3721 if (mReadOnly
) return;
3722 if (!Utf8DecoderFast
.isValidDC(dch
)) dch
= Utf8DecoderFast
.replacement
;
3723 if (dch
< 128) { doPutChar(cast(char)dch
); return; }
3724 char[4] ubuf
= void;
3725 auto len
= utf8Encode(ubuf
[], dch
);
3726 if (len
< 1) return;
3728 insertText
!"end"(curpos
, ubuf
.ptr
[0..len
]);
3730 // recode to codepage
3731 doPutChar(recodeU2B(dch
));
3736 void doPutTextUtf (const(char)[] str) {
3737 if (mReadOnly
) return;
3738 if (str.length
== 0) return;
3740 if (!mSingleLine
&& utfuck
&& inPasteMode
> 0) {
3741 insertText
!"end"(curpos
, str);
3745 bool ugstarted
= false;
3746 void startug () { if (!ugstarted
) { ugstarted
= true; undoGroupStart(); } }
3747 scope(exit
) if (ugstarted
) undoGroupEnd();
3749 Utf8DecoderFast udc
;
3750 foreach (immutable char ch
; str) {
3751 if (udc
.decodeSafe(cast(ubyte)ch
)) {
3752 dchar dch
= cast(dchar)udc
.codepoint
;
3753 if (!mSingleLine
&& dch
== '\n') { startug(); doLineSplit(inPasteMode
<= 0); continue; }
3755 doPutChar(recodeU2B(dch
));
3760 /// put text in koi8
3761 void doPutText (const(char)[] str) {
3762 if (mReadOnly
) return;
3763 if (str.length
== 0) return;
3765 if (!mSingleLine
&& !utfuck
&& codepage
== CodePage
.koi8u
&& inPasteMode
> 0) {
3766 insertText
!"end"(curpos
, str);
3770 bool ugstarted
= false;
3771 void startug () { if (!ugstarted
) { ugstarted
= true; undoGroupStart(); } }
3772 scope(exit
) if (ugstarted
) undoGroupEnd();
3776 while (pos
< str.length
) {
3778 while (pos
< str.length
) {
3780 if (!mSingleLine
&& ch
== '\n') break;
3781 if (ch
>= 128) break;
3784 if (stpos
< pos
) { startug(); insertText
!"end"(curpos
, str.ptr
[stpos
..pos
]); }
3785 if (pos
>= str.length
) break;
3787 if (!mSingleLine
&& ch
== '\n') { startug(); doLineSplit(inPasteMode
<= 0); ++pos
; continue; }
3788 if (ch
< ' ') { startug(); insertText
!"end"(curpos
, str.ptr
[pos
..pos
+1]); ++pos
; continue; }
3789 Utf8DecoderFast udc
;
3791 while (pos
< str.length
) if (udc
.decode(cast(ubyte)(str.ptr
[pos
++]))) break;
3794 insertText
!"end"(curpos
, str.ptr
[stpos
..pos
]);
3796 ch
= uni2koi(Utf8DecoderFast
.replacement
);
3797 insertText
!"end"(curpos
, (&ch
)[0..1]);
3803 void doPasteStart () {
3804 if (mKillTextOnChar
) {
3805 killTextOnChar
= false;
3806 if (gb
.textsize
> 0) {
3809 markingBlock
= false;
3810 deleteText
!"start"(0, gb
.textsize
);
3819 void doPasteEnd () {
3820 killTextOnChar
= false;
3821 if (--inPasteMode
< 0) inPasteMode
= 0;
3825 protected final bool xIndentLine (int lidx
) {
3827 if (mReadOnly
) return false;
3828 if (lidx
< 0 || lidx
>= lc
.linecount
) return false;
3829 auto pos
= lc
.xy2pos(0, lidx
);
3830 auto epos
= lc
.xy2pos(0, lidx
+1);
3832 // if line consists of blanks only, don't do anything
3833 while (pos
< epos
) {
3835 if (ch
== '\n') return true;
3836 if (ch
> ' ') break;
3839 if (pos
>= gb
.textsize
) return true;
3842 return insertText
!("none", false)(pos
, spc
[]);
3846 void doIndentBlock () {
3847 if (mReadOnly
) return;
3848 killTextOnChar
= false;
3849 if (!hasMarkedBlock
) return;
3850 int sy
= lc
.pos2line(bstart
);
3851 int ey
= lc
.pos2line(bend
-1);
3852 bool bsAtBOL
= (bstart
== lc
.line2pos(sy
));
3854 scope(exit
) undoGroupEnd();
3855 foreach (int lidx
; sy
..ey
+1) xIndentLine(lidx
);
3856 if (bsAtBOL
) bstart
= lc
.line2pos(sy
); // line already marked as dirty
3859 protected final bool xUnindentLine (int lidx
) {
3860 if (mReadOnly
) return false;
3861 if (lidx
< 0 || lidx
>= lc
.linecount
) return true;
3862 auto pos
= lc
.xy2pos(0, lidx
);
3864 if (gb
[pos
] > ' ' || gb
[pos
] == '\n') return true;
3865 if (pos
+1 < gb
.textsize
&& gb
[pos
+1] <= ' ' && gb
[pos
+1] != '\n') ++len
;
3866 return deleteText
!"none"(pos
, len
);
3870 void doUnindentBlock () {
3871 if (mReadOnly
) return;
3872 killTextOnChar
= false;
3873 if (!hasMarkedBlock
) return;
3874 int sy
= lc
.pos2line(bstart
);
3875 int ey
= lc
.pos2line(bend
-1);
3877 scope(exit
) undoGroupEnd();
3878 foreach (int lidx
; sy
..ey
+1) xUnindentLine(lidx
);
3881 // ////////////////////////////////////////////////////////////////////// //
3884 version(egeditor_record_movement_undo
) {
3885 /// push cursor position to undo stack
3886 final bool pushUndoCurPos () nothrow {
3887 return (undo
!is null ? undo
.addCurMove(this) : false);
3891 // returns old state
3892 enum SetupShiftMarkingMixin
= q
{
3893 auto omb
= markingBlock
;
3894 scope(exit
) markingBlock
= omb
;
3896 if (!hasMarkedBlock
) {
3898 bstart
= bend
= pos
;
3901 markingBlock
= true;
3906 void doWordLeft (bool domark
=false) {
3907 mixin(SetupShiftMarkingMixin
);
3908 killTextOnChar
= false;
3910 if (pos
== 0) return;
3911 auto ch
= gb
[pos
-1];
3912 if (!mSingleLine
&& ch
== '\n') { doLeft(); return; }
3918 if ((!mSingleLine
&& ch
== '\n') || ch
> ' ') break;
3922 if (stpos
> 0 && isWordChar(ch
)) {
3925 if (!isWordChar(ch
)) break;
3929 version(egeditor_record_movement_undo
) pushUndoCurPos();
3930 lc
.pos2xy(stpos
, cx
, cy
);
3935 void doWordRight (bool domark
=false) {
3936 mixin(SetupShiftMarkingMixin
);
3937 killTextOnChar
= false;
3939 if (pos
== gb
.textsize
) return;
3941 if (!mSingleLine
&& ch
== '\n') { doRight(); return; }
3945 while (epos
< gb
.textsize
) {
3947 if ((!mSingleLine
&& ch
== '\n') || ch
> ' ') break;
3950 } else if (isWordChar(ch
)) {
3951 while (epos
< gb
.textsize
) {
3953 if (!isWordChar(ch
)) {
3955 while (epos
< gb
.textsize
) {
3957 if ((!mSingleLine
&& ch
== '\n') || ch
> ' ') break;
3966 version(egeditor_record_movement_undo
) pushUndoCurPos();
3967 lc
.pos2xy(epos
, cx
, cy
);
3972 void doTextTop (bool domark
=false) {
3973 mixin(SetupShiftMarkingMixin
);
3974 if (lc
.mLineCount
< 2) return;
3975 killTextOnChar
= false;
3976 if (mTopLine
== 0 && cy
== 0) return;
3977 version(egeditor_record_movement_undo
) pushUndoCurPos();
3983 void doTextBottom (bool domark
=false) {
3984 mixin(SetupShiftMarkingMixin
);
3985 if (lc
.mLineCount
< 2) return;
3986 killTextOnChar
= false;
3987 if (cy
>= lc
.linecount
-1) return;
3988 version(egeditor_record_movement_undo
) pushUndoCurPos();
3989 cy
= lc
.linecount
-1;
3994 void doPageTop (bool domark
=false) {
3995 mixin(SetupShiftMarkingMixin
);
3996 if (lc
.mLineCount
< 2) return;
3997 killTextOnChar
= false;
3998 if (cy
== mTopLine
) return;
3999 version(egeditor_record_movement_undo
) pushUndoCurPos();
4005 void doPageBottom (bool domark
=false) {
4006 mixin(SetupShiftMarkingMixin
);
4007 if (lc
.mLineCount
< 2) return;
4008 killTextOnChar
= false;
4009 int ny
= mTopLine
+linesPerWindow
-1;
4010 if (ny
>= lc
.linecount
) ny
= lc
.linecount
-1;
4012 version(egeditor_record_movement_undo
) pushUndoCurPos();
4019 void doScrollUp (bool domark
=false) {
4020 mixin(SetupShiftMarkingMixin
);
4022 killTextOnChar
= false;
4023 version(egeditor_record_movement_undo
) pushUndoCurPos();
4026 } else if (cy
> 0) {
4027 killTextOnChar
= false;
4028 version(egeditor_record_movement_undo
) pushUndoCurPos();
4035 void doScrollDown (bool domark
=false) {
4036 mixin(SetupShiftMarkingMixin
);
4037 if (mTopLine
+linesPerWindow
< lc
.linecount
) {
4038 killTextOnChar
= false;
4039 version(egeditor_record_movement_undo
) pushUndoCurPos();
4042 } else if (cy
< lc
.linecount
-1) {
4043 killTextOnChar
= false;
4044 version(egeditor_record_movement_undo
) pushUndoCurPos();
4051 void doUp (bool domark
=false) {
4052 mixin(SetupShiftMarkingMixin
);
4054 killTextOnChar
= false;
4055 version(egeditor_record_movement_undo
) pushUndoCurPos();
4058 if (winh
>= 24 && isCurLineBeforeTop
) {
4059 mTopLine
= cy
-(winh
/3);
4060 if (mTopLine
< 0) mTopLine
= 0;
4061 setDirtyLinesLength(visibleLinesPerWindow
);
4069 void doDown (bool domark
=false) {
4070 mixin(SetupShiftMarkingMixin
);
4071 if (cy
< lc
.linecount
-1) {
4072 killTextOnChar
= false;
4073 version(egeditor_record_movement_undo
) pushUndoCurPos();
4076 if (winh
>= 24 && isCurLineAfterBottom
) {
4077 mTopLine
= cy
+(winh
/3)-linesPerWindow
+1;
4078 if (mTopLine
< 0) mTopLine
= 0;
4079 setDirtyLinesLength(visibleLinesPerWindow
);
4087 void doLeft (bool domark
=false) {
4088 mixin(SetupShiftMarkingMixin
);
4090 killTextOnChar
= false;
4091 lc
.pos2xy(curpos
, rx
, ry
);
4092 if (cx
> rx
) cx
= rx
;
4094 version(egeditor_record_movement_undo
) pushUndoCurPos();
4096 } else if (cy
> 0) {
4098 version(egeditor_record_movement_undo
) pushUndoCurPos();
4099 lc
.pos2xy(lc
.xy2pos(0, cy
)-1, cx
, cy
);
4105 void doRight (bool domark
=false) {
4106 mixin(SetupShiftMarkingMixin
);
4108 killTextOnChar
= false;
4109 lc
.pos2xy(lc
.xy2pos(cx
+1, cy
), rx
, ry
);
4111 if (cy
< lc
.linecount
-1) {
4112 version(egeditor_record_movement_undo
) pushUndoCurPos();
4117 version(egeditor_record_movement_undo
) pushUndoCurPos();
4124 void doPageUp (bool domark
=false) {
4125 mixin(SetupShiftMarkingMixin
);
4126 if (linesPerWindow
< 2 || lc
.mLineCount
< 2) return;
4127 killTextOnChar
= false;
4128 int ntl
= mTopLine
-(linesPerWindow
-1);
4129 int ncy
= cy
-(linesPerWindow
-1);
4130 if (ntl
< 0) ntl
= 0;
4131 if (ncy
< 0) ncy
= 0;
4132 if (ntl
!= mTopLine || ncy
!= cy
) {
4133 version(egeditor_record_movement_undo
) pushUndoCurPos();
4141 void doPageDown (bool domark
=false) {
4142 mixin(SetupShiftMarkingMixin
);
4143 if (linesPerWindow
< 2 || lc
.mLineCount
< 2) return;
4144 killTextOnChar
= false;
4145 int ntl
= mTopLine
+(linesPerWindow
-1);
4146 int ncy
= cy
+(linesPerWindow
-1);
4147 if (ntl
+linesPerWindow
>= lc
.linecount
) ntl
= lc
.linecount
-linesPerWindow
;
4148 if (ncy
>= lc
.linecount
) ncy
= lc
.linecount
-1;
4149 if (ntl
< 0) ntl
= 0;
4150 if (ntl
!= mTopLine || ncy
!= cy
) {
4151 version(egeditor_record_movement_undo
) pushUndoCurPos();
4159 void doHome (bool smart
=true, bool domark
=false) {
4160 mixin(SetupShiftMarkingMixin
);
4161 killTextOnChar
= false;
4163 version(egeditor_record_movement_undo
) pushUndoCurPos();
4168 auto pos
= lc
.xy2pos(0, cy
);
4169 while (pos
< gb
.textsize
) {
4171 if (!mSingleLine
&& ch
== '\n') return;
4172 if (ch
> ' ') break;
4177 version(egeditor_record_movement_undo
) pushUndoCurPos();
4185 void doEnd (bool domark
=false) {
4186 mixin(SetupShiftMarkingMixin
);
4188 killTextOnChar
= false;
4190 if (cy
>= lc
.linecount
-1) {
4193 ep
= lc
.lineend(cy
);
4194 //if (gb[ep] != '\n') ++ep; // word wrapping
4196 lc
.pos2xy(ep
, rx
, ry
);
4197 if (rx
!= cx || ry
!= cy
) {
4198 version(egeditor_record_movement_undo
) pushUndoCurPos();
4206 /*private*/protected void doUndoRedo (UndoStack us
) { // "allMembers" trait: shut the fuck up!
4207 if (us
is null) return;
4208 killTextOnChar
= false;
4210 while (us
.hasUndo
) {
4211 auto tp
= us
.undoAction(this);
4213 case UndoStack
.Type
.GroupStart
:
4214 if (--level
<= 0) return;
4216 case UndoStack
.Type
.GroupEnd
:
4220 if (level
<= 0) return;
4226 void doUndo () { doUndoRedo(undo
); } ///
4227 void doRedo () { doUndoRedo(redo
); } ///
4230 void doBlockResetMark (bool saveUndo
=true) nothrow {
4231 killTextOnChar
= false;
4232 if (bstart
< bend
) {
4233 version(egeditor_record_movement_undo
) if (saveUndo
) pushUndoCurPos();
4234 markLinesDirtySE(lc
.pos2line(bstart
), lc
.pos2line(bend
-1));
4237 markingBlock
= false;
4240 /// toggle block marking mode
4241 void doToggleBlockMarkMode () {
4242 killTextOnChar
= false;
4243 if (bstart
== bend
&& markingBlock
) { doBlockResetMark(false); return; }
4244 if (bstart
< bend
&& !markingBlock
) doBlockResetMark(false);
4246 if (!hasMarkedBlock
) {
4247 bstart
= bend
= pos
;
4248 markingBlock
= true;
4251 if (pos
!= bstart
) {
4253 if (bend
< bstart
) { pos
= bstart
; bstart
= bend
; bend
= pos
; }
4255 markingBlock
= false;
4256 dirtyLines
[] = -1; //FIXME: optimize
4261 void doSetBlockStart () {
4262 killTextOnChar
= false;
4264 if ((hasMarkedBlock ||
(bstart
== bend
&& bstart
>= 0 && bstart
< gb
.textsize
)) && pos
< bend
) {
4265 //if (pos < bstart) markRangeDirty(pos, bstart-pos); else markRangeDirty(bstart, pos-bstart);
4270 bstart
= bend
= pos
;
4273 markingBlock
= false;
4274 dirtyLines
[] = -1; //FIXME: optimize
4278 void doSetBlockEnd () {
4280 if ((hasMarkedBlock ||
(bstart
== bend
&& bstart
>= 0 && bstart
< gb
.textsize
)) && pos
> bstart
) {
4281 //if (pos < bend) markRangeDirty(pos, bend-pos); else markRangeDirty(bend, pos-bend);
4286 bstart
= bend
= pos
;
4289 markingBlock
= false;
4290 dirtyLines
[] = -1; //FIXME: optimize
4294 // called by undo/redo processors
4295 final void ubTextRemove (int pos
, int len
) {
4296 if (mReadOnly
) return;
4297 killTextOnChar
= false;
4298 int nlc
= (!mSingleLine ? gb
.countEolsInRange(pos
, len
) : 0);
4299 bookmarkDeletionFix(pos
, len
, nlc
);
4300 lineChangedByPos(pos
, (nlc
> 0));
4301 lc
.remove(pos
, len
);
4304 // called by undo/redo processors
4305 final bool ubTextInsert (int pos
, const(char)[] str) {
4306 if (mReadOnly
) return true;
4307 killTextOnChar
= false;
4308 if (str.length
== 0) return true;
4309 int nlc
= (!mSingleLine ? gb
.countEols(str) : 0);
4310 bookmarkInsertionFix(pos
, pos
+cast(int)str.length
, nlc
);
4311 if (lc
.put(pos
, str) >= 0) {
4312 lineChangedByPos(pos
, (nlc
> 0));
4319 // can be called only after `ubTextInsert`, and with the same pos/length
4320 // usually it is done by undo/redo action if the editor is in "rich mode"
4321 final void ubTextSetAttrs (int pos
, const(GapBuffer
.HighState
)[] hs
) {
4322 if (mReadOnly || hs
.length
== 0) return;
4323 assert(gb
.hasHiBuffer
);
4324 foreach (const ref hi
; hs
) gb
.hbuf
[gb
.pos2real(pos
++)] = hi
;
4327 // ////////////////////////////////////////////////////////////////////// //
4328 public static struct TextRange
{
4332 int left
; // chars left, including front
4336 this (EditorEngine aed
, int apos
, int aleft
, char afrontch
) pure {
4343 this (EditorEngine aed
, usize lo
, usize hi
) {
4345 if (aed
!is null && lo
< hi
&& lo
< aed
.gb
.textsize
) {
4347 if (hi
> ed
.gb
.textsize
) hi
= ed
.gb
.textsize
;
4348 left
= cast(int)hi
-pos
+1; // compensate for first popFront
4353 @property bool empty () const pure @safe @nogc { pragma(inline
, true); return (left
<= 0); }
4354 @property char front () const pure @safe @nogc { pragma(inline
, true); return frontch
; }
4356 if (ed
is null || left
< 2) { left
= 0; frontch
= 0; return; }
4358 if (pos
>= ed
.gb
.textsize
) { left
= 0; frontch
= 0; return; }
4359 frontch
= ed
.gb
[pos
++];
4361 auto save () pure { pragma(inline
, true); return TextRange(ed
, pos
, left
, frontch
); }
4362 @property usize
length () const pure @safe @nogc { pragma(inline
, true); return (left
> 0 ? left
: 0); }
4363 alias opDollar
= length
;
4364 char opIndex (usize idx
) {
4365 pragma(inline
, true);
4366 return (left
> 0 && idx
< left ?
(idx
== 0 ? frontch
: ed
.gb
[pos
+cast(int)idx
-1]) : 0);
4368 auto opSlice () pure { pragma(inline
, true); return this.save
; }
4369 //WARNING: logic untested!
4370 auto opSlice (uint lo
, uint hi
) {
4371 if (ed
is null || left
<= 0 || lo
>= left || lo
>= hi
) return TextRange(null, 0, 0);
4372 hi
-= lo
; // convert to length
4373 if (hi
> left
) hi
= left
;
4374 if (left
-lo
> hi
) hi
= left
-lo
;
4375 return TextRange(ed
, cast(int)lo
+1, cast(int)hi
, ed
.gb
[cast(int)lo
]);
4377 // make it bidirectional, just for fun
4378 //WARNING: completely untested!
4379 char back () const pure {
4380 pragma(inline
, true);
4381 return (ed
!is null && left
> 0 ?
(left
== 1 ? frontch
: ed
.gb
[pos
+left
-2]) : 0);
4384 if (ed
is null || left
< 2) { left
= 0; frontch
= 0; return; }
4390 /// range interface to editor text
4391 /// WARNING! do not change anything while range is active, or results *WILL* be UD
4392 final TextRange
opSlice (usize lo
, usize hi
) nothrow { return TextRange(this, lo
, hi
); }
4393 final TextRange
opSlice () nothrow { return TextRange(this, 0, gb
.textsize
); } /// ditto
4394 final int opDollar () nothrow { return gb
.textsize
; } ///
4397 final TextRange
markedBlockRange () nothrow {
4398 if (!hasMarkedBlock
) return TextRange
.init
;
4399 return TextRange(this, bstart
, bend
);
4404 bool isWordChar (char ch
) pure nothrow {
4405 return (ch
.isalnum || ch
== '_' || ch
> 127);