"C-Period" to find my messages; "C-Insert" to copy post URL to clipboard
[knntp.git] / editor.d
blob92440b427f00283b0f292f7c6b57c2918decb72c
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 bool isMarked (int x, int y) const pure nothrow @safe @nogc {
93 if (!hasMark) return false;
94 if (cy < marky) {
95 if (y < cy || y > marky) return false;
96 if (y > cy && y < marky) return true;
97 if (y == cy) return (x >= cx);
98 if (y == marky) return (x < markx);
99 assert(0, "wtf?!");
100 } else if (cy > marky) {
101 if (y < marky || y > cy) return false;
102 if (y > marky && y < cy) return true;
103 if (y == marky) return (x >= markx);
104 if (y == cy) return (x < cx);
105 assert(0, "wtf?!");
106 } else {
107 if (y != marky) return false;
108 if (cx == markx) return false;
109 return (cx < markx ? (x >= cx && x < markx) : (x >= markx && x < cx));
111 assert(0, "wtf?!");
114 bool lineHasMark (int y) const pure nothrow @safe @nogc {
115 if (!hasMark) return false;
116 if (cy == marky) return (y == cy);
117 return (cy < marky ? (y >= cy && y <= marky) : (y >= marky && y <= cy));
120 string getSelectionText () const pure nothrow @safe {
121 if (!hasMark || (cx == markx && cy == marky)) return null;
122 int sx, sy, ex, ey;
123 if (cy < marky) { sx = cx; sy = cy; ex = markx; ey = marky; }
124 else if (cy > marky) { sx = markx; sy = marky; ex = cx; ey = cy; }
125 else if (cx < markx) { sx = cx; sy = cy; ex = markx; ey = marky; }
126 else { sx = markx; sy = marky; ex = cx; ey = cy; }
127 string res;
128 while (sx != ex || sy != ey) {
129 dchar ch = chatAt(sx, sy);
130 if (ch == 0) {
131 res ~= "\n";
132 sx = 0;
133 ++sy;
134 } else {
135 res ~= ch;
136 ++sx;
139 return res;
142 // return # of chars taken by quoting
143 int quoteLength (int lidx) const pure nothrow @safe @nogc {
144 if (lidx < 0 || lidx >= lines.length) return 0;
145 string s = lines[lidx];
146 if (s.length == 0 || s[0] != '>') return 0;
147 int pos = 0;
148 while (pos < s.length) {
149 if (s[pos] == ' ') { ++pos; continue; }
150 if (s[pos] != '>') break;
151 ++pos;
153 if (pos < s.length && s[pos] == ' ') ++pos;
154 return pos;
157 int quoteLevel (int lidx) const pure nothrow @safe @nogc {
158 if (lidx < 0 || lidx >= lines.length) return 0;
159 string s = lines[lidx];
160 if (s.length == 0 || s[0] != '>') return 0;
161 int res = 0;
162 while (s.length) {
163 if (s[0] == ' ') { s = s[1..$]; continue; }
164 if (s[0] != '>') break;
165 ++res;
166 s = s[1..$];
168 return res;
171 string opIndex (int idx) const pure nothrow @safe @nogc { return (idx >= 0 && idx < lines.length ? lines[idx] : null); }
173 int linelen (int idx) const pure nothrow @safe @nogc {
174 if (idx < 0 || idx >= lines.length) return 0;
175 string s = lines[idx];
176 if (s.length == 0) return 0;
177 Utf8DecoderFast dc;
178 int res = 0;
179 while (s.length) {
180 if (dc.decode(cast(ubyte)s[0])) ++res;
181 s = s[1..$];
183 return res;
186 // x position to line offset
187 int lineofs (int x, int idx) const pure nothrow @safe @nogc {
188 if (x <= 0 || idx < 0 || idx >= lines.length) return 0;
189 string s = lines[idx];
190 if (x >= s.length) return cast(int)s.length;
191 Utf8DecoderFast dc;
192 int res = 0;
193 while (s.length) {
194 ++res;
195 if (dc.decode(cast(ubyte)s[0])) {
196 if (--x == 0) break;
198 s = s[1..$];
200 return res;
203 dchar chatAt (int x, int y) const pure nothrow @safe @nogc {
204 if (x < 0 || y < 0 || y >= lines.length) return 0;
205 string s = lines[y];
206 if (s.length == 0 || x >= s.length) return 0;
207 Utf8DecoderFast dc;
208 while (s.length) {
209 if (dc.decode(cast(ubyte)s[0])) {
210 if (x-- == 0) return dc.codepoint;
212 s = s[1..$];
214 return 0;
217 // reformat whole text
218 void reformat () {
219 cx = 0;
220 cy = 0;
222 while (cy < lines.length) {
223 if (linelen(cy) > maxlinelen) {
224 int pos = linewrap-1;
225 while (pos >= 0 && chatAt(pos, cy) > ' ') --pos;
226 if (pos > 0) {
227 cx = ++pos;
228 doEnter();
229 } else {
230 ++cy;
233 // join and rewrap
234 if (quoteLevel(cy) != quoteLevel(cy+1)) { ++cy; continue; }
235 if (lines[cy].length == 0 || lines[cy][$-1] > ' ') { ++cy; continue; }
236 doLineJoin();
239 cx = 0;
240 cy = cast(int)lines.length;
243 void putUtf (const(char)[] s) {
244 Utf8DecoderFast dc;
245 while (s.length) {
246 if (dc.decode(cast(ubyte)s[0])) {
247 if (dc.isValidDC(dc.codepoint)) putChar(dc.codepoint); else putChar('?');
249 s = s[1..$];
253 void putChar (dchar ch) {
254 if (ch == SpecCh.PutMark) { markx = cx; marky = cy; return; }
255 if (ch == SpecCh.ResetMark) { markx = 0; marky = -1; return; }
256 if (ch == 13 || ch == 10) { marky = -1; doEnter(); return; }
257 if (ch == SpecCh.Backspace) { marky = -1; doBackspace(); return; }
258 if (ch == SpecCh.Delete) { marky = -1; doDelete(); return; }
259 if (ch < ' ') {
260 switch (ch) {
261 case SpecCh.Left: doLeft(); break;
262 case SpecCh.Right: doRight(); break;
263 case SpecCh.Up: doUp(); break;
264 case SpecCh.Down: doDown(); break;
265 case SpecCh.Home: doHome(); break;
266 case SpecCh.End: doEnd(); break;
267 case SpecCh.KillLine:
268 marky = -1;
269 if (cy < lines.length) {
270 foreach (immutable c; cy+1..lines.length) lines[c-1] = lines[c];
271 lines.length -= 1;
272 lines.assumeSafeAppend;
273 cx = 0;
275 break;
276 default:
278 return;
280 marky = -1;
281 string s;
282 if (cy >= lines.length) {
283 cx = 0;
284 cy = cast(int)lines.length;
285 s ~= ch;
286 lines ~= s;
287 } else {
288 int len = linelen(cy);
289 if (cx >= len) {
290 cx = len;
291 lines[cy] ~= ch;
292 } else {
293 s = lines[cy];
294 int ofs = lineofs(cx, cy);
295 if (ofs >= s.length) {
296 s ~= ch;
297 } else {
298 string t = s[0..ofs];
299 t ~= ch;
300 s = t~s[ofs..$];
301 lines[cy] = s;
305 doRight();
306 // wrapping
307 if (linelen(cy) > linewrap) {
308 int ocx = cx, ocy = cy;
309 while (cy < lines.length) {
310 if (linelen(cy) > linewrap) {
311 int pos = linewrap-1;
312 while (pos >= 0 && chatAt(pos, cy) > ' ') --pos;
313 if (pos <= 0) break;
314 cx = ++pos;
315 doEnter();
316 } else {
317 if (lines.length-cy < 2) break;
318 // join and rewrap
319 if (quoteLevel(cy) != quoteLevel(cy+1)) break;
320 if (lines[cy].length == 0 || lines[cy][$-1] > ' ') break;
321 doLineJoin();
324 // fix cursor coordinates
325 cx = ocx;
326 cy = ocy;
327 while (cy < lines.length) {
328 if (cy == ocy) {
329 if (cx < linelen(cy)) break;
330 } else {
331 if (cx <= linelen(cy)) break;
333 cx -= linelen(cy);
334 ++cy;
335 skipQuotes();
340 private void skipQuotes () {
341 if (cy < 0 || cy >= lines.length) return;
342 string s = lines[cy];
343 if (s.length == 0 || s[0] != '>') return;
344 int pos = 0;
345 while (pos < s.length) {
346 if (s[pos] == ' ' || s[pos] == '>') {
347 ++cx;
348 ++pos;
349 } else {
350 break;
353 if (pos < s.length && s[pos] == ' ') ++cx;
356 void doLineJoin () {
357 if (cy < 0) cy = 0;
358 if (cy >= lines.length || lines.length-cy < 2) return;
359 cx = linelen(cy);
360 string s = lines[cy+1];
361 usize stpos = 0;
362 if (s.length > 0 && s[0] == '>') {
363 while (stpos < s.length) if (s[stpos] == ' ' || s[stpos] == '>') ++stpos; else break;
364 while (stpos < s.length && s[stpos] <= ' ') ++stpos;
365 s = s[stpos..$];
366 string cs = lines[cy];
367 if (cs.length == 0 || cs[$-1] != ' ') lines[cy] ~= " ";
369 lines[cy] ~= s;
370 foreach (immutable c; cy+2..lines.length) lines[c-1] = lines[c];
371 lines.length -= 1;
372 lines.assumeSafeAppend;
375 void doEnter () {
376 if (cy >= lines.length) {
377 lines ~= null;
378 cx = 0;
379 cy = cast(int)lines.length;
380 } else {
381 string s = lines[cy];
382 int ql = quoteLevel(cy);
383 int qlen = quoteLength(cy);
384 int ofs = lineofs(cx, cy);
385 lines.length += 1;
386 foreach_reverse (immutable c; cy+1..lines.length) lines[c] = lines[c-1];
387 lines[cy] = s[0..ofs];
388 if (qlen > 0) {
389 lines[cy+1] = s[0..qlen]~s[ofs..$];
390 cx = qlen; // it is ok, quote chars are never multibyte
391 } else {
392 lines[cy+1] = s[ofs..$];
393 cx = 0;
395 ++cy;
399 void doBackspace () {
400 if (cy >= lines.length) {
401 if (cx > 0) --cx;
402 return;
403 } else if (cx > 0) {
404 string s = lines[cy];
405 int ofs1 = lineofs(cx, cy);
406 int ofs0 = lineofs(--cx, cy);
407 if (ofs1 > 0) {
408 lines[cy] = s[0..ofs0];
409 if (ofs1 < s.length) lines[cy] ~= s[ofs1..$];
410 return;
413 // join
414 if (cy > 0) {
415 --cy;
416 cx = linelen(cy);
417 doLineJoin();
421 void doDelete () {
422 void doJoin () {
423 if (cy >= lines.length || lines.length-cy < 2) return;
424 cx = linelen(cy);
425 doLineJoin();
428 if (cy < lines.length) {
429 string s = lines[cy];
430 int ofs0 = lineofs(cx, cy);
431 if (ofs0 >= s.length) { doJoin(); return; }
432 int ofs1 = lineofs(cx+1, cy);
433 if (ofs0 == ofs1) { doJoin(); return; }
434 lines[cy] = s[0..ofs0];
435 if (ofs1 < s.length) lines[cy] ~= s[ofs1..$];
439 void doLeft () {
440 int len = linelen(cy);
441 if (cx > len) cx = len;
442 if (cx == 0) {
443 if (cy > 0) {
444 --cy;
445 cx = linelen(cy);
447 } else {
448 --cx;
452 void doRight () {
453 auto len = linelen(cy);
454 if (cx > len) cx = len;
455 if (cx >= len) {
456 if (cy < lines.length) { ++cy; cx = 0; }
457 } else {
458 ++cx;
462 void doUp () {
463 if (cy > 0) --cy;
466 void doDown () {
467 if (cy < lines.length) ++cy;
470 void doHome () {
471 cx = 0;
474 void doEnd () {
475 cx = linelen(cy);