Add 'src/jexer/' from commit 'cf01c92f5809a0732409e280fb0f32f27393618d'
[fanfix.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 // TODO: set different colors based on file extension
93 highlighter.setJavaColors();
94
95 String [] rawLines = str.split("\n");
96 for (int i = 0; i < rawLines.length; i++) {
97 lines.add(new Line(rawLines[i], this.defaultColor, highlighter));
98 }
99 }
100
101 // ------------------------------------------------------------------------
102 // Document ---------------------------------------------------------------
103 // ------------------------------------------------------------------------
104
105 /**
106 * Get the overwrite flag.
107 *
108 * @return true if addChar() overwrites data, false if it inserts
109 */
110 public boolean getOverwrite() {
111 return overwrite;
112 }
113
114 /**
115 * Get the dirty value.
116 *
117 * @return true if the buffer is dirty
118 */
119 public boolean isDirty() {
120 return dirty;
121 }
122
123 /**
124 * Save contents to file.
125 *
126 * @param filename file to save to
127 * @throws IOException if a java.io operation throws
128 */
129 public void saveToFilename(final String filename) throws IOException {
130 OutputStreamWriter output = null;
131 try {
132 output = new OutputStreamWriter(new FileOutputStream(filename),
133 "UTF-8");
134
135 for (Line line: lines) {
136 output.write(line.getRawString());
137 output.write("\n");
138 }
139
140 dirty = false;
141 }
142 finally {
143 if (output != null) {
144 output.close();
145 }
146 }
147 }
148
149 /**
150 * Set the overwrite flag.
151 *
152 * @param overwrite true if addChar() should overwrite data, false if it
153 * should insert
154 */
155 public void setOverwrite(final boolean overwrite) {
156 this.overwrite = overwrite;
157 }
158
159 /**
160 * Get the current line number being edited.
161 *
162 * @return the line number. Note that this is 0-based: 0 is the first
163 * line.
164 */
165 public int getLineNumber() {
166 return lineNumber;
167 }
168
169 /**
170 * Get the current editing line.
171 *
172 * @return the line
173 */
174 public Line getCurrentLine() {
175 return lines.get(lineNumber);
176 }
177
178 /**
179 * Get a specific line by number.
180 *
181 * @param lineNumber the line number. Note that this is 0-based: 0 is
182 * the first line.
183 * @return the line
184 */
185 public Line getLine(final int lineNumber) {
186 return lines.get(lineNumber);
187 }
188
189 /**
190 * Set the current line number being edited.
191 *
192 * @param n the line number. Note that this is 0-based: 0 is the first
193 * line.
194 */
195 public void setLineNumber(final int n) {
196 if ((n < 0) || (n > lines.size())) {
197 throw new IndexOutOfBoundsException("Lines array size is " +
198 lines.size() + ", requested index " + n);
199 }
200 lineNumber = n;
201 }
202
203 /**
204 * Get the current cursor position of the editing line.
205 *
206 * @return the cursor position
207 */
208 public int getCursor() {
209 return lines.get(lineNumber).getCursor();
210 }
211
212 /**
213 * Get the character at the current cursor position in the text.
214 *
215 * @return the character, or -1 if the cursor is at the end of the line
216 */
217 public int getChar() {
218 return lines.get(lineNumber).getChar();
219 }
220
221 /**
222 * Set the current cursor position of the editing line. 0-based.
223 *
224 * @param cursor the new cursor position
225 */
226 public void setCursor(final int cursor) {
227 if (cursor >= lines.get(lineNumber).getDisplayLength()) {
228 lines.get(lineNumber).end();
229 } else {
230 lines.get(lineNumber).setCursor(cursor);
231 }
232 }
233
234 /**
235 * Increment the line number by one. If at the last line, do nothing.
236 *
237 * @return true if the editing line changed
238 */
239 public boolean down() {
240 if (lineNumber < lines.size() - 1) {
241 int x = lines.get(lineNumber).getCursor();
242 lineNumber++;
243 if (x >= lines.get(lineNumber).getDisplayLength()) {
244 lines.get(lineNumber).end();
245 } else {
246 lines.get(lineNumber).setCursor(x);
247 }
248 return true;
249 }
250 return false;
251 }
252
253 /**
254 * Increment the line number by n. If n would go past the last line,
255 * increment only to the last line.
256 *
257 * @param n the number of lines to increment by
258 * @return true if the editing line changed
259 */
260 public boolean down(final int n) {
261 if (lineNumber < lines.size() - 1) {
262 int x = lines.get(lineNumber).getCursor();
263 lineNumber += n;
264 if (lineNumber > lines.size() - 1) {
265 lineNumber = lines.size() - 1;
266 }
267 if (x >= lines.get(lineNumber).getDisplayLength()) {
268 lines.get(lineNumber).end();
269 } else {
270 lines.get(lineNumber).setCursor(x);
271 }
272 return true;
273 }
274 return false;
275 }
276
277 /**
278 * Decrement the line number by one. If at the first line, do nothing.
279 *
280 * @return true if the editing line changed
281 */
282 public boolean up() {
283 if (lineNumber > 0) {
284 int x = lines.get(lineNumber).getCursor();
285 lineNumber--;
286 if (x >= lines.get(lineNumber).getDisplayLength()) {
287 lines.get(lineNumber).end();
288 } else {
289 lines.get(lineNumber).setCursor(x);
290 }
291 return true;
292 }
293 return false;
294 }
295
296 /**
297 * Decrement the line number by n. If n would go past the first line,
298 * decrement only to the first line.
299 *
300 * @param n the number of lines to decrement by
301 * @return true if the editing line changed
302 */
303 public boolean up(final int n) {
304 if (lineNumber > 0) {
305 int x = lines.get(lineNumber).getCursor();
306 lineNumber -= n;
307 if (lineNumber < 0) {
308 lineNumber = 0;
309 }
310 if (x >= lines.get(lineNumber).getDisplayLength()) {
311 lines.get(lineNumber).end();
312 } else {
313 lines.get(lineNumber).setCursor(x);
314 }
315 return true;
316 }
317 return false;
318 }
319
320 /**
321 * Decrement the cursor by one. If at the first column on the first
322 * line, do nothing.
323 *
324 * @return true if the cursor position changed
325 */
326 public boolean left() {
327 if (!lines.get(lineNumber).left()) {
328 // We are on the leftmost column, wrap
329 if (up()) {
330 end();
331 } else {
332 return false;
333 }
334 }
335 return true;
336 }
337
338 /**
339 * Increment the cursor by one. If at the last column on the last line,
340 * do nothing.
341 *
342 * @return true if the cursor position changed
343 */
344 public boolean right() {
345 if (!lines.get(lineNumber).right()) {
346 // We are on the rightmost column, wrap
347 if (down()) {
348 home();
349 } else {
350 return false;
351 }
352 }
353 return true;
354 }
355
356 /**
357 * Go back to the beginning of this word if in the middle, or the
358 * beginning of the previous word.
359 */
360 public void backwardsWord() {
361
362 // If at the beginning of a word already, push past it.
363 if ((getChar() != -1)
364 && (getRawLine().length() > 0)
365 && !Character.isSpace((char) getChar())
366 ) {
367 left();
368 }
369
370 // int line = lineNumber;
371 while ((getChar() == -1)
372 || (getRawLine().length() == 0)
373 || Character.isSpace((char) getChar())
374 ) {
375 if (left() == false) {
376 return;
377 }
378 }
379
380
381 assert (getChar() != -1);
382
383 if (!Character.isSpace((char) getChar())
384 && (getRawLine().length() > 0)
385 ) {
386 // Advance until at the beginning of the document or a whitespace
387 // is encountered.
388 while (!Character.isSpace((char) getChar())) {
389 int line = lineNumber;
390 if (left() == false) {
391 // End of document, bail out.
392 return;
393 }
394 if (lineNumber != line) {
395 // We wrapped a line. Here that counts as whitespace.
396 right();
397 return;
398 }
399 }
400 }
401
402 // We went one past the word, push back to the first character of
403 // that word.
404 right();
405 return;
406 }
407
408 /**
409 * Go to the beginning of the next word.
410 */
411 public void forwardsWord() {
412 int line = lineNumber;
413 while ((getChar() == -1)
414 || (getRawLine().length() == 0)
415 ) {
416 if (right() == false) {
417 return;
418 }
419 if (lineNumber != line) {
420 // We wrapped a line. Here that counts as whitespace.
421 if (!Character.isSpace((char) getChar())) {
422 // We found a character immediately after the line.
423 // Done!
424 return;
425 }
426 // Still looking...
427 line = lineNumber;
428 }
429 }
430 assert (getChar() != -1);
431
432 if (!Character.isSpace((char) getChar())
433 && (getRawLine().length() > 0)
434 ) {
435 // Advance until at the end of the document or a whitespace is
436 // encountered.
437 while (!Character.isSpace((char) getChar())) {
438 line = lineNumber;
439 if (right() == false) {
440 // End of document, bail out.
441 return;
442 }
443 if (lineNumber != line) {
444 // We wrapped a line. Here that counts as whitespace.
445 if (!Character.isSpace((char) getChar())
446 && (getRawLine().length() > 0)
447 ) {
448 // We found a character immediately after the line.
449 // Done!
450 return;
451 }
452 break;
453 }
454 }
455 }
456
457 while ((getChar() == -1)
458 || (getRawLine().length() == 0)
459 ) {
460 if (right() == false) {
461 return;
462 }
463 if (lineNumber != line) {
464 // We wrapped a line. Here that counts as whitespace.
465 if (!Character.isSpace((char) getChar())) {
466 // We found a character immediately after the line.
467 // Done!
468 return;
469 }
470 // Still looking...
471 line = lineNumber;
472 }
473 }
474 assert (getChar() != -1);
475
476 if (Character.isSpace((char) getChar())) {
477 // Advance until at the end of the document or a non-whitespace
478 // is encountered.
479 while (Character.isSpace((char) getChar())) {
480 if (right() == false) {
481 // End of document, bail out.
482 return;
483 }
484 }
485 return;
486 }
487
488 // We wrapped the line to get here.
489 return;
490 }
491
492 /**
493 * Get the raw string that matches this line.
494 *
495 * @return the string
496 */
497 public String getRawLine() {
498 return lines.get(lineNumber).getRawString();
499 }
500
501 /**
502 * Go to the first column of this line.
503 *
504 * @return true if the cursor position changed
505 */
506 public boolean home() {
507 return lines.get(lineNumber).home();
508 }
509
510 /**
511 * Go to the last column of this line.
512 *
513 * @return true if the cursor position changed
514 */
515 public boolean end() {
516 return lines.get(lineNumber).end();
517 }
518
519 /**
520 * Delete the character under the cursor.
521 */
522 public void del() {
523 dirty = true;
524 int cursor = lines.get(lineNumber).getCursor();
525 if (cursor < lines.get(lineNumber).getDisplayLength() - 1) {
526 lines.get(lineNumber).del();
527 } else if (lineNumber < lines.size() - 2) {
528 // Join two lines
529 StringBuilder newLine = new StringBuilder(lines.
530 get(lineNumber).getRawString());
531 newLine.append(lines.get(lineNumber + 1).getRawString());
532 lines.set(lineNumber, new Line(newLine.toString(),
533 defaultColor, highlighter));
534 lines.get(lineNumber).setCursor(cursor);
535 lines.remove(lineNumber + 1);
536 }
537 }
538
539 /**
540 * Delete the character immediately preceeding the cursor.
541 */
542 public void backspace() {
543 dirty = true;
544 int cursor = lines.get(lineNumber).getCursor();
545 if (cursor > 0) {
546 lines.get(lineNumber).backspace();
547 } else if (lineNumber > 0) {
548 // Join two lines
549 lineNumber--;
550 String firstLine = lines.get(lineNumber).getRawString();
551 if (firstLine.length() > 0) {
552 // Backspacing combining two lines
553 StringBuilder newLine = new StringBuilder(firstLine);
554 newLine.append(lines.get(lineNumber + 1).getRawString());
555 lines.set(lineNumber, new Line(newLine.toString(),
556 defaultColor, highlighter));
557 lines.get(lineNumber).setCursor(firstLine.length());
558 lines.remove(lineNumber + 1);
559 } else {
560 // Backspacing an empty line
561 lines.remove(lineNumber);
562 lines.get(lineNumber).setCursor(0);
563 }
564 }
565 }
566
567 /**
568 * Split the current line into two, like pressing the enter key.
569 */
570 public void enter() {
571 dirty = true;
572 int cursor = lines.get(lineNumber).getRawCursor();
573 String original = lines.get(lineNumber).getRawString();
574 String firstLine = original.substring(0, cursor);
575 String secondLine = original.substring(cursor);
576 lines.add(lineNumber + 1, new Line(secondLine, defaultColor,
577 highlighter));
578 lines.set(lineNumber, new Line(firstLine, defaultColor, highlighter));
579 lineNumber++;
580 lines.get(lineNumber).home();
581 }
582
583 /**
584 * Replace or insert a character at the cursor, depending on overwrite
585 * flag.
586 *
587 * @param ch the character to replace or insert
588 */
589 public void addChar(final int ch) {
590 dirty = true;
591 if (overwrite) {
592 lines.get(lineNumber).replaceChar(ch);
593 } else {
594 lines.get(lineNumber).addChar(ch);
595 }
596 }
597
598 /**
599 * Get a (shallow) copy of the list of lines.
600 *
601 * @return the list of lines
602 */
603 public List<Line> getLines() {
604 return new ArrayList<Line>(lines);
605 }
606
607 /**
608 * Get the number of lines.
609 *
610 * @return the number of lines
611 */
612 public int getLineCount() {
613 return lines.size();
614 }
615
616 /**
617 * Compute the maximum line length for this document.
618 *
619 * @return the number of cells needed to display the longest line
620 */
621 public int getLineLengthMax() {
622 int n = 0;
623 for (Line line : lines) {
624 if (line.getDisplayLength() > n) {
625 n = line.getDisplayLength();
626 }
627 }
628 return n;
629 }
630
631 /**
632 * Get the current line length.
633 *
634 * @return the number of cells needed to display the current line
635 */
636 public int getLineLength() {
637 return lines.get(lineNumber).getDisplayLength();
638 }
639
640 }