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 // Set colors to resemble the Borland IDE colors, but for Java
94 highlighter
.setJavaColors();
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
));
102 // ------------------------------------------------------------------------
103 // Document ---------------------------------------------------------------
104 // ------------------------------------------------------------------------
107 * Get the overwrite flag.
109 * @return true if addChar() overwrites data, false if it inserts
111 public boolean getOverwrite() {
116 * Get the dirty value.
118 * @return true if the buffer is dirty
120 public boolean isDirty() {
125 * Unset the dirty flag.
127 public void setNotDirty() {
132 * Save contents to file.
134 * @param filename file to save to
135 * @throws IOException if a java.io operation throws
137 public void saveToFilename(final String filename
) throws IOException
{
138 OutputStreamWriter output
= null;
140 output
= new OutputStreamWriter(new FileOutputStream(filename
),
143 for (Line line
: lines
) {
144 output
.write(line
.getRawString());
151 if (output
!= null) {
158 * Set the overwrite flag.
160 * @param overwrite true if addChar() should overwrite data, false if it
163 public void setOverwrite(final boolean overwrite
) {
164 this.overwrite
= overwrite
;
168 * Get the current line number being edited.
170 * @return the line number. Note that this is 0-based: 0 is the first
173 public int getLineNumber() {
178 * Get the current editing line.
182 public Line
getCurrentLine() {
183 return lines
.get(lineNumber
);
187 * Get a specific line by number.
189 * @param lineNumber the line number. Note that this is 0-based: 0 is
193 public Line
getLine(final int lineNumber
) {
194 return lines
.get(lineNumber
);
198 * Set the current line number being edited.
200 * @param n the line number. Note that this is 0-based: 0 is the first
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
);
212 * Get the current cursor position of the editing line.
214 * @return the cursor position
216 public int getCursor() {
217 return lines
.get(lineNumber
).getCursor();
221 * Get the character at the current cursor position in the text.
223 * @return the character, or -1 if the cursor is at the end of the line
225 public int getChar() {
226 return lines
.get(lineNumber
).getChar();
230 * Set the current cursor position of the editing line. 0-based.
232 * @param cursor the new cursor position
234 public void setCursor(final int cursor
) {
235 if (cursor
>= lines
.get(lineNumber
).getDisplayLength()) {
236 lines
.get(lineNumber
).end();
238 lines
.get(lineNumber
).setCursor(cursor
);
243 * Increment the line number by one. If at the last line, do nothing.
245 * @return true if the editing line changed
247 public boolean down() {
248 if (lineNumber
< lines
.size() - 1) {
249 int x
= lines
.get(lineNumber
).getCursor();
251 if (x
>= lines
.get(lineNumber
).getDisplayLength()) {
252 lines
.get(lineNumber
).end();
254 lines
.get(lineNumber
).setCursor(x
);
262 * Increment the line number by n. If n would go past the last line,
263 * increment only to the last line.
265 * @param n the number of lines to increment by
266 * @return true if the editing line changed
268 public boolean down(final int n
) {
269 if (lineNumber
< lines
.size() - 1) {
270 int x
= lines
.get(lineNumber
).getCursor();
272 if (lineNumber
> lines
.size() - 1) {
273 lineNumber
= lines
.size() - 1;
275 if (x
>= lines
.get(lineNumber
).getDisplayLength()) {
276 lines
.get(lineNumber
).end();
278 lines
.get(lineNumber
).setCursor(x
);
286 * Decrement the line number by one. If at the first line, do nothing.
288 * @return true if the editing line changed
290 public boolean up() {
291 if (lineNumber
> 0) {
292 int x
= lines
.get(lineNumber
).getCursor();
294 if (x
>= lines
.get(lineNumber
).getDisplayLength()) {
295 lines
.get(lineNumber
).end();
297 lines
.get(lineNumber
).setCursor(x
);
305 * Decrement the line number by n. If n would go past the first line,
306 * decrement only to the first line.
308 * @param n the number of lines to decrement by
309 * @return true if the editing line changed
311 public boolean up(final int n
) {
312 if (lineNumber
> 0) {
313 int x
= lines
.get(lineNumber
).getCursor();
315 if (lineNumber
< 0) {
318 if (x
>= lines
.get(lineNumber
).getDisplayLength()) {
319 lines
.get(lineNumber
).end();
321 lines
.get(lineNumber
).setCursor(x
);
329 * Decrement the cursor by one. If at the first column on the first
332 * @return true if the cursor position changed
334 public boolean left() {
335 if (!lines
.get(lineNumber
).left()) {
336 // We are on the leftmost column, wrap
347 * Increment the cursor by one. If at the last column on the last line,
350 * @return true if the cursor position changed
352 public boolean right() {
353 if (!lines
.get(lineNumber
).right()) {
354 // We are on the rightmost column, wrap
365 * Go back to the beginning of this word if in the middle, or the
366 * beginning of the previous word.
368 public void backwardsWord() {
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())
378 // int line = lineNumber;
379 while ((getChar() == -1)
380 || (getRawLine().length() == 0)
381 || Character
.isWhitespace((char) getChar())
383 if (left() == false) {
389 assert (getChar() != -1);
391 if (!Character
.isWhitespace((char) getChar())
392 && (getRawLine().length() > 0)
394 // Advance until at the beginning of the document or a whitespace
396 while (!Character
.isWhitespace((char) getChar())) {
397 int line
= lineNumber
;
398 if (left() == false) {
399 // End of document, bail out.
402 if (lineNumber
!= line
) {
403 // We wrapped a line. Here that counts as whitespace.
410 // We went one past the word, push back to the first character of
417 * Go to the beginning of the next word.
419 public void forwardsWord() {
420 int line
= lineNumber
;
421 while ((getChar() == -1)
422 || (getRawLine().length() == 0)
424 if (right() == false) {
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.
438 assert (getChar() != -1);
440 if (!Character
.isWhitespace((char) getChar())
441 && (getRawLine().length() > 0)
443 // Advance until at the end of the document or a whitespace is
445 while (!Character
.isWhitespace((char) getChar())) {
447 if (right() == false) {
448 // End of document, bail out.
451 if (lineNumber
!= line
) {
452 // We wrapped a line. Here that counts as whitespace.
453 if (!Character
.isWhitespace((char) getChar())
454 && (getRawLine().length() > 0)
456 // We found a character immediately after the line.
465 while ((getChar() == -1)
466 || (getRawLine().length() == 0)
468 if (right() == false) {
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.
482 assert (getChar() != -1);
484 if (Character
.isWhitespace((char) getChar())) {
485 // Advance until at the end of the document or a non-whitespace
487 while (Character
.isWhitespace((char) getChar())) {
488 if (right() == false) {
489 // End of document, bail out.
496 // We wrapped the line to get here.
501 * Get the raw string that matches this line.
505 public String
getRawLine() {
506 return lines
.get(lineNumber
).getRawString();
510 * Go to the first column of this line.
512 * @return true if the cursor position changed
514 public boolean home() {
515 return lines
.get(lineNumber
).home();
519 * Go to the last column of this line.
521 * @return true if the cursor position changed
523 public boolean end() {
524 return lines
.get(lineNumber
).end();
528 * Delete the character under the cursor.
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) {
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);
548 * Delete the character immediately preceeding the cursor.
550 public void backspace() {
552 int cursor
= lines
.get(lineNumber
).getCursor();
554 lines
.get(lineNumber
).backspace();
555 } else if (lineNumber
> 0) {
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);
568 // Backspacing an empty line
569 lines
.remove(lineNumber
);
570 lines
.get(lineNumber
).setCursor(0);
576 * Split the current line into two, like pressing the enter key.
578 public void enter() {
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
,
586 lines
.set(lineNumber
, new Line(firstLine
, defaultColor
, highlighter
));
588 lines
.get(lineNumber
).home();
592 * Replace or insert a character at the cursor, depending on overwrite
595 * @param ch the character to replace or insert
597 public void addChar(final int ch
) {
600 lines
.get(lineNumber
).replaceChar(ch
);
602 lines
.get(lineNumber
).addChar(ch
);
607 * Get a (shallow) copy of the list of lines.
609 * @return the list of lines
611 public List
<Line
> getLines() {
612 return new ArrayList
<Line
>(lines
);
616 * Get the number of lines.
618 * @return the number of lines
620 public int getLineCount() {
625 * Compute the maximum line length for this document.
627 * @return the number of cells needed to display the longest line
629 public int getLineLengthMax() {
631 for (Line line
: lines
) {
632 if (line
.getDisplayLength() > n
) {
633 n
= line
.getDisplayLength();
640 * Get the current line length.
642 * @return the number of cells needed to display the current line
644 public int getLineLength() {
645 return lines
.get(lineNumber
).getDisplayLength();
649 * Get the entire contents of the document as one string.
651 * @return the document contents
653 public String
getText() {
654 StringBuilder sb
= new StringBuilder();
655 for (Line line
: getLines()) {
656 sb
.append(line
.getRawString());
659 return sb
.toString();