2 * Jexer - Java Text User Interface
4 * The MIT License (MIT)
6 * Copyright (C) 2019 Kevin Lamonte
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:
15 * The above copyright notice and this permission notice shall be included in
16 * all copies or substantial portions of the Software.
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.
26 * @author Kevin Lamonte [kevin.lamonte@gmail.com]
29 package jexer
.teditor
;
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
;
37 import jexer
.bits
.CellAttributes
;
40 * A Document represents a text file, as a collection of lines.
42 public class Document
{
44 // ------------------------------------------------------------------------
45 // Variables --------------------------------------------------------------
46 // ------------------------------------------------------------------------
51 private ArrayList
<Line
> lines
= new ArrayList
<Line
>();
54 * The current line number being edited. Note that this is 0-based, the
55 * first line is line number 0.
57 private int lineNumber
= 0;
60 * The overwrite flag. When true, characters overwrite data.
62 private boolean overwrite
= false;
65 * If true, the document has been edited.
67 private boolean dirty
= false;
70 * The default color for the TEditor class.
72 private CellAttributes defaultColor
= null;
75 * The text highlighter to use.
77 private Highlighter highlighter
= new Highlighter();
79 // ------------------------------------------------------------------------
80 // Constructors -----------------------------------------------------------
81 // ------------------------------------------------------------------------
84 * Construct a new Document from an existing text string.
86 * @param str the text string
87 * @param defaultColor the color for unhighlighted text
89 public Document(final String str
, final CellAttributes defaultColor
) {
90 this.defaultColor
= defaultColor
;
92 // TODO: set different colors based on file extension
93 highlighter
.setJavaColors();
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
));
101 // ------------------------------------------------------------------------
102 // Document ---------------------------------------------------------------
103 // ------------------------------------------------------------------------
106 * Get the overwrite flag.
108 * @return true if addChar() overwrites data, false if it inserts
110 public boolean getOverwrite() {
115 * Get the dirty value.
117 * @return true if the buffer is dirty
119 public boolean isDirty() {
124 * Save contents to file.
126 * @param filename file to save to
127 * @throws IOException if a java.io operation throws
129 public void saveToFilename(final String filename
) throws IOException
{
130 OutputStreamWriter output
= null;
132 output
= new OutputStreamWriter(new FileOutputStream(filename
),
135 for (Line line
: lines
) {
136 output
.write(line
.getRawString());
143 if (output
!= null) {
150 * Set the overwrite flag.
152 * @param overwrite true if addChar() should overwrite data, false if it
155 public void setOverwrite(final boolean overwrite
) {
156 this.overwrite
= overwrite
;
160 * Get the current line number being edited.
162 * @return the line number. Note that this is 0-based: 0 is the first
165 public int getLineNumber() {
170 * Get the current editing line.
174 public Line
getCurrentLine() {
175 return lines
.get(lineNumber
);
179 * Get a specific line by number.
181 * @param lineNumber the line number. Note that this is 0-based: 0 is
185 public Line
getLine(final int lineNumber
) {
186 return lines
.get(lineNumber
);
190 * Set the current line number being edited.
192 * @param n the line number. Note that this is 0-based: 0 is the first
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
);
204 * Get the current cursor position of the editing line.
206 * @return the cursor position
208 public int getCursor() {
209 return lines
.get(lineNumber
).getCursor();
213 * Get the character at the current cursor position in the text.
215 * @return the character, or -1 if the cursor is at the end of the line
217 public int getChar() {
218 return lines
.get(lineNumber
).getChar();
222 * Set the current cursor position of the editing line. 0-based.
224 * @param cursor the new cursor position
226 public void setCursor(final int cursor
) {
227 if (cursor
>= lines
.get(lineNumber
).getDisplayLength()) {
228 lines
.get(lineNumber
).end();
230 lines
.get(lineNumber
).setCursor(cursor
);
235 * Increment the line number by one. If at the last line, do nothing.
237 * @return true if the editing line changed
239 public boolean down() {
240 if (lineNumber
< lines
.size() - 1) {
241 int x
= lines
.get(lineNumber
).getCursor();
243 if (x
>= lines
.get(lineNumber
).getDisplayLength()) {
244 lines
.get(lineNumber
).end();
246 lines
.get(lineNumber
).setCursor(x
);
254 * Increment the line number by n. If n would go past the last line,
255 * increment only to the last line.
257 * @param n the number of lines to increment by
258 * @return true if the editing line changed
260 public boolean down(final int n
) {
261 if (lineNumber
< lines
.size() - 1) {
262 int x
= lines
.get(lineNumber
).getCursor();
264 if (lineNumber
> lines
.size() - 1) {
265 lineNumber
= lines
.size() - 1;
267 if (x
>= lines
.get(lineNumber
).getDisplayLength()) {
268 lines
.get(lineNumber
).end();
270 lines
.get(lineNumber
).setCursor(x
);
278 * Decrement the line number by one. If at the first line, do nothing.
280 * @return true if the editing line changed
282 public boolean up() {
283 if (lineNumber
> 0) {
284 int x
= lines
.get(lineNumber
).getCursor();
286 if (x
>= lines
.get(lineNumber
).getDisplayLength()) {
287 lines
.get(lineNumber
).end();
289 lines
.get(lineNumber
).setCursor(x
);
297 * Decrement the line number by n. If n would go past the first line,
298 * decrement only to the first line.
300 * @param n the number of lines to decrement by
301 * @return true if the editing line changed
303 public boolean up(final int n
) {
304 if (lineNumber
> 0) {
305 int x
= lines
.get(lineNumber
).getCursor();
307 if (lineNumber
< 0) {
310 if (x
>= lines
.get(lineNumber
).getDisplayLength()) {
311 lines
.get(lineNumber
).end();
313 lines
.get(lineNumber
).setCursor(x
);
321 * Decrement the cursor by one. If at the first column on the first
324 * @return true if the cursor position changed
326 public boolean left() {
327 if (!lines
.get(lineNumber
).left()) {
328 // We are on the leftmost column, wrap
339 * Increment the cursor by one. If at the last column on the last line,
342 * @return true if the cursor position changed
344 public boolean right() {
345 if (!lines
.get(lineNumber
).right()) {
346 // We are on the rightmost column, wrap
357 * Go back to the beginning of this word if in the middle, or the
358 * beginning of the previous word.
360 public void backwardsWord() {
362 // If at the beginning of a word already, push past it.
363 if ((getChar() != -1)
364 && (getRawLine().length() > 0)
365 && !Character
.isWhitespace((char) getChar())
370 // int line = lineNumber;
371 while ((getChar() == -1)
372 || (getRawLine().length() == 0)
373 || Character
.isWhitespace((char) getChar())
375 if (left() == false) {
381 assert (getChar() != -1);
383 if (!Character
.isWhitespace((char) getChar())
384 && (getRawLine().length() > 0)
386 // Advance until at the beginning of the document or a whitespace
388 while (!Character
.isWhitespace((char) getChar())) {
389 int line
= lineNumber
;
390 if (left() == false) {
391 // End of document, bail out.
394 if (lineNumber
!= line
) {
395 // We wrapped a line. Here that counts as whitespace.
402 // We went one past the word, push back to the first character of
409 * Go to the beginning of the next word.
411 public void forwardsWord() {
412 int line
= lineNumber
;
413 while ((getChar() == -1)
414 || (getRawLine().length() == 0)
416 if (right() == false) {
419 if (lineNumber
!= line
) {
420 // We wrapped a line. Here that counts as whitespace.
421 if (!Character
.isWhitespace((char) getChar())) {
422 // We found a character immediately after the line.
430 assert (getChar() != -1);
432 if (!Character
.isWhitespace((char) getChar())
433 && (getRawLine().length() > 0)
435 // Advance until at the end of the document or a whitespace is
437 while (!Character
.isWhitespace((char) getChar())) {
439 if (right() == false) {
440 // End of document, bail out.
443 if (lineNumber
!= line
) {
444 // We wrapped a line. Here that counts as whitespace.
445 if (!Character
.isWhitespace((char) getChar())
446 && (getRawLine().length() > 0)
448 // We found a character immediately after the line.
457 while ((getChar() == -1)
458 || (getRawLine().length() == 0)
460 if (right() == false) {
463 if (lineNumber
!= line
) {
464 // We wrapped a line. Here that counts as whitespace.
465 if (!Character
.isWhitespace((char) getChar())) {
466 // We found a character immediately after the line.
474 assert (getChar() != -1);
476 if (Character
.isWhitespace((char) getChar())) {
477 // Advance until at the end of the document or a non-whitespace
479 while (Character
.isWhitespace((char) getChar())) {
480 if (right() == false) {
481 // End of document, bail out.
488 // We wrapped the line to get here.
493 * Get the raw string that matches this line.
497 public String
getRawLine() {
498 return lines
.get(lineNumber
).getRawString();
502 * Go to the first column of this line.
504 * @return true if the cursor position changed
506 public boolean home() {
507 return lines
.get(lineNumber
).home();
511 * Go to the last column of this line.
513 * @return true if the cursor position changed
515 public boolean end() {
516 return lines
.get(lineNumber
).end();
520 * Delete the character under the cursor.
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) {
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);
540 * Delete the character immediately preceeding the cursor.
542 public void backspace() {
544 int cursor
= lines
.get(lineNumber
).getCursor();
546 lines
.get(lineNumber
).backspace();
547 } else if (lineNumber
> 0) {
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);
560 // Backspacing an empty line
561 lines
.remove(lineNumber
);
562 lines
.get(lineNumber
).setCursor(0);
568 * Split the current line into two, like pressing the enter key.
570 public void enter() {
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
,
578 lines
.set(lineNumber
, new Line(firstLine
, defaultColor
, highlighter
));
580 lines
.get(lineNumber
).home();
584 * Replace or insert a character at the cursor, depending on overwrite
587 * @param ch the character to replace or insert
589 public void addChar(final int ch
) {
592 lines
.get(lineNumber
).replaceChar(ch
);
594 lines
.get(lineNumber
).addChar(ch
);
599 * Get a (shallow) copy of the list of lines.
601 * @return the list of lines
603 public List
<Line
> getLines() {
604 return new ArrayList
<Line
>(lines
);
608 * Get the number of lines.
610 * @return the number of lines
612 public int getLineCount() {
617 * Compute the maximum line length for this document.
619 * @return the number of cells needed to display the longest line
621 public int getLineLengthMax() {
623 for (Line line
: lines
) {
624 if (line
.getDisplayLength() > n
) {
625 n
= line
.getDisplayLength();
632 * Get the current line length.
634 * @return the number of cells needed to display the current line
636 public int getLineLength() {
637 return lines
.get(lineNumber
).getDisplayLength();