resync from TJIDE
[fanfix.git] / src / jexer / teditor / Document.java
CommitLineData
12b55d76
KL
1/*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
a69ed767 6 * Copyright (C) 2019 Kevin Lamonte
12b55d76
KL
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 */
29package jexer.teditor;
30
71a389c9
KL
31import java.io.FileOutputStream;
32import java.io.IOException;
33import java.io.OutputStreamWriter;
12b55d76
KL
34import java.util.ArrayList;
35import java.util.List;
36
e8a11f98
KL
37import jexer.bits.CellAttributes;
38
12b55d76
KL
39/**
40 * A Document represents a text file, as a collection of lines.
41 */
42public class Document {
43
615a0d99
KL
44 // ------------------------------------------------------------------------
45 // Variables --------------------------------------------------------------
46 // ------------------------------------------------------------------------
47
12b55d76
KL
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
71a389c9
KL
64 /**
65 * If true, the document has been edited.
66 */
67 private boolean dirty = false;
68
e8a11f98
KL
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
615a0d99
KL
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
edbcdccc
KL
92 // Set colors to resemble the Borland IDE colors, but for Java
93 // language keywords.
615a0d99
KL
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
12b55d76
KL
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
71a389c9
KL
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
0580bf2c
KL
124 /**
125 * Unset the dirty flag.
126 */
127 public void setNotDirty() {
128 dirty = false;
129 }
130
71a389c9
KL
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
12b55d76
KL
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
e8a11f98
KL
177 /**
178 * Get the current editing line.
179 *
180 * @return the line
181 */
182 public Line getCurrentLine() {
183 return lines.get(lineNumber);
184 }
185
12b55d76
KL
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())) {
e8a11f98
KL
205 throw new IndexOutOfBoundsException("Lines array size is " +
206 lines.size() + ", requested index " + n);
12b55d76
KL
207 }
208 lineNumber = n;
209 }
210
e8a11f98
KL
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
e23989a4
KL
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
71a389c9
KL
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) {
4297b49b
KL
235 if (cursor >= lines.get(lineNumber).getDisplayLength()) {
236 lines.get(lineNumber).end();
237 } else {
238 lines.get(lineNumber).setCursor(cursor);
239 }
71a389c9
KL
240 }
241
12b55d76
KL
242 /**
243 * Increment the line number by one. If at the last line, do nothing.
e8a11f98
KL
244 *
245 * @return true if the editing line changed
12b55d76 246 */
e8a11f98 247 public boolean down() {
12b55d76 248 if (lineNumber < lines.size() - 1) {
e8a11f98 249 int x = lines.get(lineNumber).getCursor();
12b55d76 250 lineNumber++;
4297b49b 251 if (x >= lines.get(lineNumber).getDisplayLength()) {
e8a11f98
KL
252 lines.get(lineNumber).end();
253 } else {
254 lines.get(lineNumber).setCursor(x);
255 }
256 return true;
12b55d76 257 }
e8a11f98 258 return false;
12b55d76
KL
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
e8a11f98 266 * @return true if the editing line changed
12b55d76 267 */
e8a11f98
KL
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 }
4297b49b 275 if (x >= lines.get(lineNumber).getDisplayLength()) {
e8a11f98
KL
276 lines.get(lineNumber).end();
277 } else {
278 lines.get(lineNumber).setCursor(x);
279 }
280 return true;
12b55d76 281 }
e8a11f98 282 return false;
12b55d76
KL
283 }
284
285 /**
286 * Decrement the line number by one. If at the first line, do nothing.
e8a11f98
KL
287 *
288 * @return true if the editing line changed
12b55d76 289 */
e8a11f98 290 public boolean up() {
12b55d76 291 if (lineNumber > 0) {
e8a11f98 292 int x = lines.get(lineNumber).getCursor();
12b55d76 293 lineNumber--;
4297b49b 294 if (x >= lines.get(lineNumber).getDisplayLength()) {
e8a11f98
KL
295 lines.get(lineNumber).end();
296 } else {
297 lines.get(lineNumber).setCursor(x);
298 }
299 return true;
12b55d76 300 }
e8a11f98 301 return false;
12b55d76
KL
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
e8a11f98 309 * @return true if the editing line changed
12b55d76 310 */
e8a11f98
KL
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 }
4297b49b 318 if (x >= lines.get(lineNumber).getDisplayLength()) {
e8a11f98
KL
319 lines.get(lineNumber).end();
320 } else {
321 lines.get(lineNumber).setCursor(x);
322 }
323 return true;
12b55d76 324 }
e8a11f98 325 return false;
12b55d76
KL
326 }
327
328 /**
e23989a4
KL
329 * Decrement the cursor by one. If at the first column on the first
330 * line, do nothing.
e8a11f98
KL
331 *
332 * @return true if the cursor position changed
12b55d76 333 */
e8a11f98 334 public boolean left() {
df602ccf
KL
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;
12b55d76
KL
344 }
345
346 /**
e23989a4
KL
347 * Increment the cursor by one. If at the last column on the last line,
348 * do nothing.
e8a11f98
KL
349 *
350 * @return true if the cursor position changed
12b55d76 351 */
e8a11f98 352 public boolean right() {
df602ccf
KL
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;
12b55d76
KL
362 }
363
e23989a4
KL
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)
b2efac6e 373 && !Character.isWhitespace((char) getChar())
e23989a4
KL
374 ) {
375 left();
376 }
377
378 // int line = lineNumber;
379 while ((getChar() == -1)
380 || (getRawLine().length() == 0)
b2efac6e 381 || Character.isWhitespace((char) getChar())
e23989a4
KL
382 ) {
383 if (left() == false) {
384 return;
385 }
386 }
387
388
389 assert (getChar() != -1);
390
b2efac6e 391 if (!Character.isWhitespace((char) getChar())
e23989a4
KL
392 && (getRawLine().length() > 0)
393 ) {
394 // Advance until at the beginning of the document or a whitespace
395 // is encountered.
b2efac6e 396 while (!Character.isWhitespace((char) getChar())) {
e23989a4
KL
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.
b2efac6e 429 if (!Character.isWhitespace((char) getChar())) {
e23989a4
KL
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
b2efac6e 440 if (!Character.isWhitespace((char) getChar())
e23989a4
KL
441 && (getRawLine().length() > 0)
442 ) {
443 // Advance until at the end of the document or a whitespace is
444 // encountered.
b2efac6e 445 while (!Character.isWhitespace((char) getChar())) {
e23989a4
KL
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.
b2efac6e 453 if (!Character.isWhitespace((char) getChar())
e23989a4
KL
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.
b2efac6e 473 if (!Character.isWhitespace((char) getChar())) {
e23989a4
KL
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
b2efac6e 484 if (Character.isWhitespace((char) getChar())) {
e23989a4
KL
485 // Advance until at the end of the document or a non-whitespace
486 // is encountered.
b2efac6e 487 while (Character.isWhitespace((char) getChar())) {
e23989a4
KL
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
12b55d76
KL
509 /**
510 * Go to the first column of this line.
e8a11f98
KL
511 *
512 * @return true if the cursor position changed
12b55d76 513 */
e8a11f98
KL
514 public boolean home() {
515 return lines.get(lineNumber).home();
12b55d76
KL
516 }
517
518 /**
519 * Go to the last column of this line.
e8a11f98
KL
520 *
521 * @return true if the cursor position changed
12b55d76 522 */
e8a11f98
KL
523 public boolean end() {
524 return lines.get(lineNumber).end();
12b55d76
KL
525 }
526
527 /**
528 * Delete the character under the cursor.
529 */
530 public void del() {
71a389c9 531 dirty = true;
df602ccf
KL
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 }
12b55d76
KL
545 }
546
547 /**
548 * Delete the character immediately preceeding the cursor.
549 */
550 public void backspace() {
71a389c9 551 dirty = true;
df602ccf
KL
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;
39e86397 580 int cursor = lines.get(lineNumber).getRawCursor();
df602ccf
KL
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();
12b55d76
KL
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 */
2d3f60d8 597 public void addChar(final int ch) {
71a389c9 598 dirty = true;
e8a11f98
KL
599 if (overwrite) {
600 lines.get(lineNumber).replaceChar(ch);
601 } else {
602 lines.get(lineNumber).addChar(ch);
603 }
12b55d76
KL
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
fe0770f9
KL
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
0580bf2c
KL
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
12b55d76 662}