editor fixes
[knntp.git] / editor.d
blob1a4dd7549a5a85014bc61cfd906a76157a20dc36
1 /* DigitalMars NNTP reader
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 module editor is aliced;
20 import iv.cmdcon;
21 import iv.strex;
22 import iv.utfutil;
23 import iv.vfs;
26 // ////////////////////////////////////////////////////////////////////////// //
27 // very simple wrapping text editor, without much features
28 class Editor {
29 private:
30 int linewrap = 72; // "normal" line length
31 int maxlinelen = 79; // maximum line length
33 string[] lines;
35 int cx, cy;
36 int markx, marky = -1;
38 public:
39 enum SpecCh : dchar {
40 Left = '\x01',
41 Right = '\x02',
42 Up = '\x03',
43 Down = '\x04',
44 Home = '\x05',
45 End = '\x06',
46 PageUp = '\x07',
47 Backspace = '\x08',
48 Tab = '\x09',
49 Enter = '\x0a',
50 PageDown = '\x0b',
51 KillLine = '\x0c',
52 PutMark = '\x0e',
53 ResetMark = '\x0f',
54 Delete = cast(dchar)127,
57 private:
58 @property bool hasMark () const pure nothrow @safe @nogc { return (marky >= 0); }
60 int xy2pos (int x, int y) const pure nothrow @safe @nogc {
61 if (y < 0) return 0;
62 if (y >= lines.length) y = cast(int)lines.length;
63 uint pos = 0;
64 foreach (immutable yy; 0..y-1) pos += linelen(yy)+1; // 1 for virtual EOL
65 if (x > 0 && y < lines.length) {
66 Utf8DecoderFast dc;
67 string s = lines[y];
68 while (s.length) {
69 if (dc.decode(cast(ubyte)s[0])) {
70 ++pos;
71 if (--x == 0) break;
73 s = s[1..$];
76 return pos;
79 public:
80 this () {
83 void addLine (const(char)[] s) {
84 lines ~= s.idup;
87 @property int lineCount () const pure nothrow @safe @nogc { return cast(int)lines.length; }
89 @property int curx () const pure nothrow @safe @nogc { return cx; }
90 @property int cury () const pure nothrow @safe @nogc { return cy; }
92 void resetMark () pure nothrow @safe @nogc { markx = 0; marky = -1; }
94 bool isMarked (int x, int y) const pure nothrow @safe @nogc {
95 if (!hasMark) return false;
96 if (cy < marky) {
97 if (y < cy || y > marky) return false;
98 if (y > cy && y < marky) return true;
99 if (y == cy) return (x >= cx);
100 if (y == marky) return (x < markx);
101 assert(0, "wtf?!");
102 } else if (cy > marky) {
103 if (y < marky || y > cy) return false;
104 if (y > marky && y < cy) return true;
105 if (y == marky) return (x >= markx);
106 if (y == cy) return (x < cx);
107 assert(0, "wtf?!");
108 } else {
109 if (y != marky) return false;
110 if (cx == markx) return false;
111 return (cx < markx ? (x >= cx && x < markx) : (x >= markx && x < cx));
113 assert(0, "wtf?!");
116 bool lineHasMark (int y) const pure nothrow @safe @nogc {
117 if (!hasMark) return false;
118 if (cy == marky) return (y == cy);
119 return (cy < marky ? (y >= cy && y <= marky) : (y >= marky && y <= cy));
122 string getSelectionText () const pure nothrow @safe {
123 if (!hasMark || (cx == markx && cy == marky)) return null;
124 int sx, sy, ex, ey;
125 if (cy < marky) { sx = cx; sy = cy; ex = markx; ey = marky; }
126 else if (cy > marky) { sx = markx; sy = marky; ex = cx; ey = cy; }
127 else if (cx < markx) { sx = cx; sy = cy; ex = markx; ey = marky; }
128 else { sx = markx; sy = marky; ex = cx; ey = cy; }
129 string res;
130 while (sx != ex || sy != ey) {
131 dchar ch = chatAt(sx, sy);
132 if (ch == 0) {
133 res ~= "\n";
134 sx = 0;
135 ++sy;
136 } else {
137 res ~= ch;
138 ++sx;
141 return res;
144 // return # of chars taken by quoting
145 int quoteLength (int lidx) const pure nothrow @safe @nogc {
146 if (lidx < 0 || lidx >= lines.length) return 0;
147 string s = lines[lidx];
148 if (s.length == 0 || s[0] != '>') return 0;
149 int pos = 0;
150 while (pos < s.length) {
151 if (s[pos] == ' ') { ++pos; continue; }
152 if (s[pos] != '>') break;
153 ++pos;
155 if (pos < s.length && s[pos] == ' ') ++pos;
156 return pos;
159 int quoteLevel (int lidx) const pure nothrow @safe @nogc {
160 if (lidx < 0 || lidx >= lines.length) return 0;
161 string s = lines[lidx];
162 if (s.length == 0 || s[0] != '>') return 0;
163 int res = 0;
164 while (s.length) {
165 if (s[0] == ' ') { s = s[1..$]; continue; }
166 if (s[0] != '>') break;
167 ++res;
168 s = s[1..$];
170 return res;
173 string opIndex (int idx) const pure nothrow @safe @nogc { return (idx >= 0 && idx < lines.length ? lines[idx] : null); }
175 int linelen (int idx) const pure nothrow @safe @nogc {
176 if (idx < 0 || idx >= lines.length) return 0;
177 string s = lines[idx];
178 if (s.length == 0) return 0;
179 Utf8DecoderFast dc;
180 int res = 0;
181 while (s.length) {
182 if (dc.decode(cast(ubyte)s[0])) ++res;
183 s = s[1..$];
185 return res;
188 // x position to line offset
189 int lineofs (int x, int idx) const pure nothrow @safe @nogc {
190 if (x <= 0 || idx < 0 || idx >= lines.length) return 0;
191 string s = lines[idx];
192 if (x >= s.length) return cast(int)s.length;
193 Utf8DecoderFast dc;
194 int res = 0;
195 while (s.length) {
196 ++res;
197 if (dc.decode(cast(ubyte)s[0])) {
198 if (--x == 0) break;
200 s = s[1..$];
202 return res;
205 dchar chatAt (int x, int y) const pure nothrow @safe @nogc {
206 if (x < 0 || y < 0 || y >= lines.length) return 0;
207 string s = lines[y];
208 if (s.length == 0 || x >= s.length) return 0;
209 Utf8DecoderFast dc;
210 while (s.length) {
211 if (dc.decode(cast(ubyte)s[0])) {
212 if (x-- == 0) return dc.codepoint;
214 s = s[1..$];
216 return 0;
219 // reformat whole text
220 void reformat () {
221 cx = 0;
222 cy = 0;
224 while (cy < lines.length) {
225 if (linelen(cy) > maxlinelen) {
226 int pos = linewrap-1;
227 while (pos >= 0 && chatAt(pos, cy) > ' ') --pos;
228 if (pos > 0) {
229 cx = ++pos;
230 doEnter();
231 } else {
232 ++cy;
235 // join and rewrap
236 if (quoteLevel(cy) != quoteLevel(cy+1)) { ++cy; continue; }
237 if (lines[cy].length == 0 || lines[cy][$-1] > ' ') { ++cy; continue; }
238 doLineJoin();
241 cx = 0;
242 cy = cast(int)lines.length;
245 void putUtf (const(char)[] s) {
246 Utf8DecoderFast dc;
247 foreach (char ch; s) {
248 if (dc.decode(cast(ubyte)ch)) {
249 if (dc.isValidDC(dc.codepoint)) putChar(dc.codepoint); else putChar('?');
254 void putChar (dchar ch) {
255 switch (ch) {
256 case SpecCh.Left: doLeft(); return;
257 case SpecCh.Right: doRight(); return;
258 case SpecCh.Up: doUp(); return;
259 case SpecCh.Down: doDown(); return;
260 case SpecCh.Home: doHome(); return;
261 case SpecCh.End: doEnd(); return;
262 case SpecCh.KillLine:
263 resetMark();
264 if (cy < lines.length) {
265 foreach (immutable c; cy+1..lines.length) lines[c-1] = lines[c];
266 lines.length -= 1;
267 lines.assumeSafeAppend;
268 cx = 0;
270 return;
271 case SpecCh.PutMark: markx = cx; marky = cy; return;
272 case SpecCh.ResetMark: resetMark(); return;
273 case 13: case 10: resetMark(); doEnter!false(); return;
274 case SpecCh.Backspace: resetMark(); doBackspace(); return;
275 case SpecCh.Delete: resetMark(); doDelete(); return;
276 default:
278 if (ch < ' ' || ch == 127) return;
279 resetMark();
280 string s;
281 if (cy >= lines.length) {
282 cx = 0;
283 cy = cast(int)lines.length;
284 s ~= ch;
285 lines ~= s;
286 } else {
287 int len = linelen(cy);
288 if (cx >= len) {
289 cx = len;
290 lines[cy] ~= ch;
291 } else {
292 s = lines[cy];
293 int ofs = lineofs(cx, cy);
294 if (ofs >= s.length) {
295 s ~= ch;
296 } else {
297 string t = s[0..ofs];
298 t ~= ch;
299 s = t~s[ofs..$];
300 lines[cy] = s;
304 doRight();
305 // wrapping
306 if (linelen(cy) > linewrap) {
307 int ocx = cx, ocy = cy;
308 while (cy < lines.length) {
309 if (linelen(cy) > linewrap) {
310 int pos = linewrap-1;
311 while (pos >= 0 && chatAt(pos, cy) > ' ') --pos;
312 if (pos <= 0) break;
313 cx = ++pos;
314 doEnter();
315 } else {
316 if (lines.length-cy < 2) break;
317 // join and rewrap
318 if (quoteLevel(cy) != quoteLevel(cy+1)) break;
319 if (lines[cy].length == 0 || lines[cy][$-1] > ' ') break;
320 doLineJoin();
323 // fix cursor coordinates
324 cx = ocx;
325 cy = ocy;
326 while (cy < lines.length) {
327 if (cy == ocy) {
328 if (cx < linelen(cy)) break;
329 } else {
330 if (cx <= linelen(cy)) break;
332 cx -= linelen(cy);
333 ++cy;
334 skipQuotes();
339 private void skipQuotes () {
340 if (cy < 0 || cy >= lines.length) return;
341 string s = lines[cy];
342 if (s.length == 0 || s[0] != '>') return;
343 int pos = 0;
344 while (pos < s.length) {
345 if (s[pos] == ' ' || s[pos] == '>') {
346 ++cx;
347 ++pos;
348 } else {
349 break;
352 if (pos < s.length && s[pos] == ' ') ++cx;
355 void doLineJoin () {
356 if (cy < 0) cy = 0;
357 if (cy >= lines.length || lines.length-cy < 2) return;
358 cx = linelen(cy);
359 string s = lines[cy+1];
360 usize stpos = 0;
361 if (s.length > 0 && s[0] == '>') {
362 while (stpos < s.length) if (s[stpos] == ' ' || s[stpos] == '>') ++stpos; else break;
363 while (stpos < s.length && s[stpos] <= ' ') ++stpos;
364 s = s[stpos..$];
365 string cs = lines[cy];
366 if (cs.length == 0 || cs[$-1] != ' ') lines[cy] ~= " ";
368 lines[cy] ~= s;
369 foreach (immutable c; cy+2..lines.length) lines[c-1] = lines[c];
370 lines.length -= 1;
371 lines.assumeSafeAppend;
374 void doEnter(bool forcequotes=true) () {
375 if (cy >= lines.length) {
376 lines ~= null;
377 cx = 0;
378 cy = cast(int)lines.length;
379 } else {
380 string s = lines[cy];
381 int ql = quoteLevel(cy);
382 int qlen = quoteLength(cy);
383 int ofs = lineofs(cx, cy);
384 lines.length += 1;
385 foreach_reverse (immutable c; cy+1..lines.length) lines[c] = lines[c-1];
386 lines[cy] = s[0..ofs];
387 if (qlen > 0 && (forcequotes || cx > 0)) {
388 lines[cy+1] = s[0..qlen]~s[ofs..$];
389 cx = qlen; // it is ok, quote chars are never multibyte
390 } else {
391 lines[cy+1] = s[ofs..$];
392 cx = 0;
394 ++cy;
398 void doBackspace () {
399 if (cy >= lines.length) {
400 if (cx > 0) --cx;
401 return;
402 } else if (cx > 0) {
403 string s = lines[cy];
404 int ofs1 = lineofs(cx, cy);
405 int ofs0 = lineofs(--cx, cy);
406 if (ofs1 > 0) {
407 lines[cy] = s[0..ofs0];
408 if (ofs1 < s.length) lines[cy] ~= s[ofs1..$];
409 return;
412 // join
413 if (cy > 0) {
414 --cy;
415 cx = linelen(cy);
416 doLineJoin();
420 void doDelete () {
421 void doJoin () {
422 if (cy >= lines.length || lines.length-cy < 2) return;
423 cx = linelen(cy);
424 doLineJoin();
427 if (cy < lines.length) {
428 string s = lines[cy];
429 int ofs0 = lineofs(cx, cy);
430 if (ofs0 >= s.length) { doJoin(); return; }
431 int ofs1 = lineofs(cx+1, cy);
432 if (ofs0 == ofs1) { doJoin(); return; }
433 lines[cy] = s[0..ofs0];
434 if (ofs1 < s.length) lines[cy] ~= s[ofs1..$];
438 void doLeft () {
439 int len = linelen(cy);
440 if (cx > len) cx = len;
441 if (cx == 0) {
442 if (cy > 0) {
443 --cy;
444 cx = linelen(cy);
446 } else {
447 --cx;
451 void doRight () {
452 auto len = linelen(cy);
453 if (cx > len) cx = len;
454 if (cx >= len) {
455 if (cy < lines.length) { ++cy; cx = 0; }
456 } else {
457 ++cx;
461 void doUp () {
462 if (cy > 0) --cy;
465 void doDown () {
466 if (cy < lines.length) ++cy;
469 void doHome () {
470 cx = 0;
473 void doEnd () {
474 cx = linelen(cy);
477 void gotoTop () {
478 cx = 0;
479 cy = 0;
482 void gotoBottom () {
483 cx = 0;
484 cy = cast(int)lines.length;
489 // ////////////////////////////////////////////////////////////////////////// //
490 struct QuoteInfo {
491 int level; // quote level
492 int length; // quote prefix length, in chars
495 QuoteInfo calcQuote(T:const(char)[]) (T s) {
496 static if (is(T == typeof(null))) {
497 return QuoteInfo();
498 } else {
499 QuoteInfo qi;
500 if (s.length > 0 && s[0] == '>') {
501 while (qi.length < s.length) {
502 if (s[qi.length] != ' ') {
503 if (s[qi.length] != '>') break;
504 ++qi.level;
506 ++qi.length;
508 if (s.length-qi.length > 1 && s[qi.length] == ' ') ++qi.length;
510 return qi;