resync from TJIDE
[nikiroo-utils.git] / src / jexer / teditor / Document.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 Kevin Lamonte
7 *
8 * Permission is hereby granted, free of charge, to any person obtaining a
9 * copy of this software and associated documentation files (the "Software"),
10 * to deal in the Software without restriction, including without limitation
11 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
12 * and/or sell copies of the Software, and to permit persons to whom the
13 * Software is furnished to do so, subject to the following conditions:
14 *
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
17 *
18 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
21 * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
24 * DEALINGS IN THE SOFTWARE.
25 *
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
27 * @version 1
28 */
29 package jexer.teditor;
30
31 import java.io.FileOutputStream;
32 import java.io.IOException;
33 import java.io.OutputStreamWriter;
34 import java.util.ArrayList;
35 import java.util.List;
36
37 import jexer.bits.CellAttributes;
38
39 /**
40 * A Document represents a text file, as a collection of lines.
41 */
42 public class Document {
43
44 // ------------------------------------------------------------------------
45 // Variables --------------------------------------------------------------
46 // ------------------------------------------------------------------------
47
48 /**
49 * The list of lines.
50 */
51 private ArrayList<Line> lines = new ArrayList<Line>();
52
53 /**
54 * The current line number being edited. Note that this is 0-based, the
55 * first line is line number 0.
56 */
57 private int lineNumber = 0;
58
59 /**
60 * The overwrite flag. When true, characters overwrite data.
61 */
62 private boolean overwrite = false;
63
64 /**
65 * If true, the document has been edited.
66 */
67 private boolean dirty = false;
68
69 /**
70 * The default color for the TEditor class.
71 */
72 private CellAttributes defaultColor = null;
73
74 /**
75 * The text highlighter to use.
76 */
77 private Highlighter highlighter = new Highlighter();
78
79 // ------------------------------------------------------------------------
80 // Constructors -----------------------------------------------------------
81 // ------------------------------------------------------------------------
82
83 /**
84 * Construct a new Document from an existing text string.
85 *
86 * @param str the text string
87 * @param defaultColor the color for unhighlighted text
88 */
89 public Document(final String str, final CellAttributes defaultColor) {
90 this.defaultColor = defaultColor;
91
92 // Set colors to resemble the Borland IDE colors, but for Java
93 // language keywords.
94 highlighter.setJavaColors();
95
96 String [] rawLines = str.split("\n");
97 for (int i = 0; i < rawLines.length; i++) {
98 lines.add(new Line(rawLines[i], this.defaultColor, highlighter));
99 }
100 }
101
102 // ------------------------------------------------------------------------
103 // Document ---------------------------------------------------------------
104 // ------------------------------------------------------------------------
105
106 /**
107 * Get the overwrite flag.
108 *
109 * @return true if addChar() overwrites data, false if it inserts
110 */
111 public boolean getOverwrite() {
112 return overwrite;
113 }
114
115 /**
116 * Get the dirty value.
117 *
118 * @return true if the buffer is dirty
119 */
120 public boolean isDirty() {
121 return dirty;
122 }
123
124 /**
125 * Unset the dirty flag.
126 */
127 public void setNotDirty() {
128 dirty = false;
129 }
130
131 /**
132 * Save contents to file.
133 *
134 * @param filename file to save to
135 * @throws IOException if a java.io operation throws
136 */
137 public void saveToFilename(final String filename) throws IOException {
138 OutputStreamWriter output = null;
139 try {
140 output = new OutputStreamWriter(new FileOutputStream(filename),
141 "UTF-8");
142
143 for (Line line: lines) {
144 output.write(line.getRawString());
145 output.write("\n");
146 }
147
148 dirty = false;
149 }
150 finally {
151 if (output != null) {
152 output.close();
153 }
154 }
155 }
156
157 /**
158 * Set the overwrite flag.
159 *
160 * @param overwrite true if addChar() should overwrite data, false if it
161 * should insert
162 */
163 public void setOverwrite(final boolean overwrite) {
164 this.overwrite = overwrite;
165 }
166
167 /**
168 * Get the current line number being edited.
169 *
170 * @return the line number. Note that this is 0-based: 0 is the first
171 * line.
172 */
173 public int getLineNumber() {
174 return lineNumber;
175 }
176
177 /**
178 * Get the current editing line.
179 *
180 * @return the line
181 */
182 public Line getCurrentLine() {
183 return lines.get(lineNumber);
184 }
185
186 /**
187 * Get a specific line by number.
188 *
189 * @param lineNumber the line number. Note that this is 0-based: 0 is
190 * the first line.
191 * @return the line
192 */
193 public Line getLine(final int lineNumber) {
194 return lines.get(lineNumber);
195 }
196
197 /**
198 * Set the current line number being edited.
199 *
200 * @param n the line number. Note that this is 0-based: 0 is the first
201 * line.
202 */
203 public void setLineNumber(final int n) {
204 if ((n < 0) || (n > lines.size())) {
205 throw new IndexOutOfBoundsException("Lines array size is " +
206 lines.size() + ", requested index " + n);
207 }
208 lineNumber = n;
209 }
210
211 /**
212 * Get the current cursor position of the editing line.
213 *
214 * @return the cursor position
215 */
216 public int getCursor() {
217 return lines.get(lineNumber).getCursor();
218 }
219
220 /**
221 * Get the character at the current cursor position in the text.
222 *
223 * @return the character, or -1 if the cursor is at the end of the line
224 */
225 public int getChar() {
226 return lines.get(lineNumber).getChar();
227 }
228
229 /**
230 * Set the current cursor position of the editing line. 0-based.
231 *
232 * @param cursor the new cursor position
233 */
234 public void setCursor(final int cursor) {
235 if (cursor >= lines.get(lineNumber).getDisplayLength()) {
236 lines.get(lineNumber).end();
237 } else {
238 lines.get(lineNumber).setCursor(cursor);
239 }
240 }
241
242 /**
243 * Increment the line number by one. If at the last line, do nothing.
244 *
245 * @return true if the editing line changed
246 */
247 public boolean down() {
248 if (lineNumber < lines.size() - 1) {
249 int x = lines.get(lineNumber).getCursor();
250 lineNumber++;
251 if (x >= lines.get(lineNumber).getDisplayLength()) {
252 lines.get(lineNumber).end();
253 } else {
254 lines.get(lineNumber).setCursor(x);
255 }
256 return true;
257 }
258 return false;
259 }
260
261 /**
262 * Increment the line number by n. If n would go past the last line,
263 * increment only to the last line.
264 *
265 * @param n the number of lines to increment by
266 * @return true if the editing line changed
267 */
268 public boolean down(final int n) {
269 if (lineNumber < lines.size() - 1) {
270 int x = lines.get(lineNumber).getCursor();
271 lineNumber += n;
272 if (lineNumber > lines.size() - 1) {
273 lineNumber = lines.size() - 1;
274 }
275 if (x >= lines.get(lineNumber).getDisplayLength()) {
276 lines.get(lineNumber).end();
277 } else {
278 lines.get(lineNumber).setCursor(x);
279 }
280 return true;
281 }
282 return false;
283 }
284
285 /**
286 * Decrement the line number by one. If at the first line, do nothing.
287 *
288 * @return true if the editing line changed
289 */
290 public boolean up() {
291 if (lineNumber > 0) {
292 int x = lines.get(lineNumber).getCursor();
293 lineNumber--;
294 if (x >= lines.get(lineNumber).getDisplayLength()) {
295 lines.get(lineNumber).end();
296 } else {
297 lines.get(lineNumber).setCursor(x);
298 }
299 return true;
300 }
301 return false;
302 }
303
304 /**
305 * Decrement the line number by n. If n would go past the first line,
306 * decrement only to the first line.
307 *
308 * @param n the number of lines to decrement by
309 * @return true if the editing line changed
310 */
311 public boolean up(final int n) {
312 if (lineNumber > 0) {
313 int x = lines.get(lineNumber).getCursor();
314 lineNumber -= n;
315 if (lineNumber < 0) {
316 lineNumber = 0;
317 }
318 if (x >= lines.get(lineNumber).getDisplayLength()) {
319 lines.get(lineNumber).end();
320 } else {
321 lines.get(lineNumber).setCursor(x);
322 }
323 return true;
324 }
325 return false;
326 }
327
328 /**
329 * Decrement the cursor by one. If at the first column on the first
330 * line, do nothing.
331 *
332 * @return true if the cursor position changed
333 */
334 public boolean left() {
335 if (!lines.get(lineNumber).left()) {
336 // We are on the leftmost column, wrap
337 if (up()) {
338 end();
339 } else {
340 return false;
341 }
342 }
343 return true;
344 }
345
346 /**
347 * Increment the cursor by one. If at the last column on the last line,
348 * do nothing.
349 *
350 * @return true if the cursor position changed
351 */
352 public boolean right() {
353 if (!lines.get(lineNumber).right()) {
354 // We are on the rightmost column, wrap
355 if (down()) {
356 home();
357 } else {
358 return false;
359 }
360 }
361 return true;
362 }
363
364 /**
365 * Go back to the beginning of this word if in the middle, or the
366 * beginning of the previous word.
367 */
368 public void backwardsWord() {
369
370 // If at the beginning of a word already, push past it.
371 if ((getChar() != -1)
372 && (getRawLine().length() > 0)
373 && !Character.isWhitespace((char) getChar())
374 ) {
375 left();
376 }
377
378 // int line = lineNumber;
379 while ((getChar() == -1)
380 || (getRawLine().length() == 0)
381 || Character.isWhitespace((char) getChar())
382 ) {
383 if (left() == false) {
384 return;
385 }
386 }
387
388
389 assert (getChar() != -1);
390
391 if (!Character.isWhitespace((char) getChar())
392 && (getRawLine().length() > 0)
393 ) {
394 // Advance until at the beginning of the document or a whitespace
395 // is encountered.
396 while (!Character.isWhitespace((char) getChar())) {
397 int line = lineNumber;
398 if (left() == false) {
399 // End of document, bail out.
400 return;
401 }
402 if (lineNumber != line) {
403 // We wrapped a line. Here that counts as whitespace.
404 right();
405 return;
406 }
407 }
408 }
409
410 // We went one past the word, push back to the first character of
411 // that word.
412 right();
413 return;
414 }
415
416 /**
417 * Go to the beginning of the next word.
418 */
419 public void forwardsWord() {
420 int line = lineNumber;
421 while ((getChar() == -1)
422 || (getRawLine().length() == 0)
423 ) {
424 if (right() == false) {
425 return;
426 }
427 if (lineNumber != line) {
428 // We wrapped a line. Here that counts as whitespace.
429 if (!Character.isWhitespace((char) getChar())) {
430 // We found a character immediately after the line.
431 // Done!
432 return;
433 }
434 // Still looking...
435 line = lineNumber;
436 }
437 }
438 assert (getChar() != -1);
439
440 if (!Character.isWhitespace((char) getChar())
441 && (getRawLine().length() > 0)
442 ) {
443 // Advance until at the end of the document or a whitespace is
444 // encountered.
445 while (!Character.isWhitespace((char) getChar())) {
446 line = lineNumber;
447 if (right() == false) {
448 // End of document, bail out.
449 return;
450 }
451 if (lineNumber != line) {
452 // We wrapped a line. Here that counts as whitespace.
453 if (!Character.isWhitespace((char) getChar())
454 && (getRawLine().length() > 0)
455 ) {
456 // We found a character immediately after the line.
457 // Done!
458 return;
459 }
460 break;
461 }
462 }
463 }
464
465 while ((getChar() == -1)
466 || (getRawLine().length() == 0)
467 ) {
468 if (right() == false) {
469 return;
470 }
471 if (lineNumber != line) {
472 // We wrapped a line. Here that counts as whitespace.
473 if (!Character.isWhitespace((char) getChar())) {
474 // We found a character immediately after the line.
475 // Done!
476 return;
477 }
478 // Still looking...
479 line = lineNumber;
480 }
481 }
482 assert (getChar() != -1);
483
484 if (Character.isWhitespace((char) getChar())) {
485 // Advance until at the end of the document or a non-whitespace
486 // is encountered.
487 while (Character.isWhitespace((char) getChar())) {
488 if (right() == false) {
489 // End of document, bail out.
490 return;
491 }
492 }
493 return;
494 }
495
496 // We wrapped the line to get here.
497 return;
498 }
499
500 /**
501 * Get the raw string that matches this line.
502 *
503 * @return the string
504 */
505 public String getRawLine() {
506 return lines.get(lineNumber).getRawString();
507 }
508
509 /**
510 * Go to the first column of this line.
511 *
512 * @return true if the cursor position changed
513 */
514 public boolean home() {
515 return lines.get(lineNumber).home();
516 }
517
518 /**
519 * Go to the last column of this line.
520 *
521 * @return true if the cursor position changed
522 */
523 public boolean end() {
524 return lines.get(lineNumber).end();
525 }
526
527 /**
528 * Delete the character under the cursor.
529 */
530 public void del() {
531 dirty = true;
532 int cursor = lines.get(lineNumber).getCursor();
533 if (cursor < lines.get(lineNumber).getDisplayLength() - 1) {
534 lines.get(lineNumber).del();
535 } else if (lineNumber < lines.size() - 2) {
536 // Join two lines
537 StringBuilder newLine = new StringBuilder(lines.
538 get(lineNumber).getRawString());
539 newLine.append(lines.get(lineNumber + 1).getRawString());
540 lines.set(lineNumber, new Line(newLine.toString(),
541 defaultColor, highlighter));
542 lines.get(lineNumber).setCursor(cursor);
543 lines.remove(lineNumber + 1);
544 }
545 }
546
547 /**
548 * Delete the character immediately preceeding the cursor.
549 */
550 public void backspace() {
551 dirty = true;
552 int cursor = lines.get(lineNumber).getCursor();
553 if (cursor > 0) {
554 lines.get(lineNumber).backspace();
555 } else if (lineNumber > 0) {
556 // Join two lines
557 lineNumber--;
558 String firstLine = lines.get(lineNumber).getRawString();
559 if (firstLine.length() > 0) {
560 // Backspacing combining two lines
561 StringBuilder newLine = new StringBuilder(firstLine);
562 newLine.append(lines.get(lineNumber + 1).getRawString());
563 lines.set(lineNumber, new Line(newLine.toString(),
564 defaultColor, highlighter));
565 lines.get(lineNumber).setCursor(firstLine.length());
566 lines.remove(lineNumber + 1);
567 } else {
568 // Backspacing an empty line
569 lines.remove(lineNumber);
570 lines.get(lineNumber).setCursor(0);
571 }
572 }
573 }
574
575 /**
576 * Split the current line into two, like pressing the enter key.
577 */
578 public void enter() {
579 dirty = true;
580 int cursor = lines.get(lineNumber).getRawCursor();
581 String original = lines.get(lineNumber).getRawString();
582 String firstLine = original.substring(0, cursor);
583 String secondLine = original.substring(cursor);
584 lines.add(lineNumber + 1, new Line(secondLine, defaultColor,
585 highlighter));
586 lines.set(lineNumber, new Line(firstLine, defaultColor, highlighter));
587 lineNumber++;
588 lines.get(lineNumber).home();
589 }
590
591 /**
592 * Replace or insert a character at the cursor, depending on overwrite
593 * flag.
594 *
595 * @param ch the character to replace or insert
596 */
597 public void addChar(final int ch) {
598 dirty = true;
599 if (overwrite) {
600 lines.get(lineNumber).replaceChar(ch);
601 } else {
602 lines.get(lineNumber).addChar(ch);
603 }
604 }
605
606 /**
607 * Get a (shallow) copy of the list of lines.
608 *
609 * @return the list of lines
610 */
611 public List<Line> getLines() {
612 return new ArrayList<Line>(lines);
613 }
614
615 /**
616 * Get the number of lines.
617 *
618 * @return the number of lines
619 */
620 public int getLineCount() {
621 return lines.size();
622 }
623
624 /**
625 * Compute the maximum line length for this document.
626 *
627 * @return the number of cells needed to display the longest line
628 */
629 public int getLineLengthMax() {
630 int n = 0;
631 for (Line line : lines) {
632 if (line.getDisplayLength() > n) {
633 n = line.getDisplayLength();
634 }
635 }
636 return n;
637 }
638
639 /**
640 * Get the current line length.
641 *
642 * @return the number of cells needed to display the current line
643 */
644 public int getLineLength() {
645 return lines.get(lineNumber).getDisplayLength();
646 }
647
648 /**
649 * Get the entire contents of the document as one string.
650 *
651 * @return the document contents
652 */
653 public String getText() {
654 StringBuilder sb = new StringBuilder();
655 for (Line line: getLines()) {
656 sb.append(line.getRawString());
657 sb.append("\n");
658 }
659 return sb.toString();
660 }
661
662 }