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 * Save contents to file.
127 * @param filename file to save to
128 * @throws IOException if a java.io operation throws
130 public void saveToFilename(final String filename
) throws IOException
{
131 OutputStreamWriter output
= null;
133 output
= new OutputStreamWriter(new FileOutputStream(filename
),
136 for (Line line
: lines
) {
137 output
.write(line
.getRawString());
144 if (output
!= null) {
151 * Set the overwrite flag.
153 * @param overwrite true if addChar() should overwrite data, false if it
156 public void setOverwrite(final boolean overwrite
) {
157 this.overwrite
= overwrite
;
161 * Get the current line number being edited.
163 * @return the line number. Note that this is 0-based: 0 is the first
166 public int getLineNumber() {
171 * Get the current editing line.
175 public Line
getCurrentLine() {
176 return lines
.get(lineNumber
);
180 * Get a specific line by number.
182 * @param lineNumber the line number. Note that this is 0-based: 0 is
186 public Line
getLine(final int lineNumber
) {
187 return lines
.get(lineNumber
);
191 * Set the current line number being edited.
193 * @param n the line number. Note that this is 0-based: 0 is the first
196 public void setLineNumber(final int n
) {
197 if ((n
< 0) || (n
> lines
.size())) {
198 throw new IndexOutOfBoundsException("Lines array size is " +
199 lines
.size() + ", requested index " + n
);
205 * Get the current cursor position of the editing line.
207 * @return the cursor position
209 public int getCursor() {
210 return lines
.get(lineNumber
).getCursor();
214 * Get the character at the current cursor position in the text.
216 * @return the character, or -1 if the cursor is at the end of the line
218 public int getChar() {
219 return lines
.get(lineNumber
).getChar();
223 * Set the current cursor position of the editing line. 0-based.
225 * @param cursor the new cursor position
227 public void setCursor(final int cursor
) {
228 if (cursor
>= lines
.get(lineNumber
).getDisplayLength()) {
229 lines
.get(lineNumber
).end();
231 lines
.get(lineNumber
).setCursor(cursor
);
236 * Increment the line number by one. If at the last line, do nothing.
238 * @return true if the editing line changed
240 public boolean down() {
241 if (lineNumber
< lines
.size() - 1) {
242 int x
= lines
.get(lineNumber
).getCursor();
244 if (x
>= lines
.get(lineNumber
).getDisplayLength()) {
245 lines
.get(lineNumber
).end();
247 lines
.get(lineNumber
).setCursor(x
);
255 * Increment the line number by n. If n would go past the last line,
256 * increment only to the last line.
258 * @param n the number of lines to increment by
259 * @return true if the editing line changed
261 public boolean down(final int n
) {
262 if (lineNumber
< lines
.size() - 1) {
263 int x
= lines
.get(lineNumber
).getCursor();
265 if (lineNumber
> lines
.size() - 1) {
266 lineNumber
= lines
.size() - 1;
268 if (x
>= lines
.get(lineNumber
).getDisplayLength()) {
269 lines
.get(lineNumber
).end();
271 lines
.get(lineNumber
).setCursor(x
);
279 * Decrement the line number by one. If at the first line, do nothing.
281 * @return true if the editing line changed
283 public boolean up() {
284 if (lineNumber
> 0) {
285 int x
= lines
.get(lineNumber
).getCursor();
287 if (x
>= lines
.get(lineNumber
).getDisplayLength()) {
288 lines
.get(lineNumber
).end();
290 lines
.get(lineNumber
).setCursor(x
);
298 * Decrement the line number by n. If n would go past the first line,
299 * decrement only to the first line.
301 * @param n the number of lines to decrement by
302 * @return true if the editing line changed
304 public boolean up(final int n
) {
305 if (lineNumber
> 0) {
306 int x
= lines
.get(lineNumber
).getCursor();
308 if (lineNumber
< 0) {
311 if (x
>= lines
.get(lineNumber
).getDisplayLength()) {
312 lines
.get(lineNumber
).end();
314 lines
.get(lineNumber
).setCursor(x
);
322 * Decrement the cursor by one. If at the first column on the first
325 * @return true if the cursor position changed
327 public boolean left() {
328 if (!lines
.get(lineNumber
).left()) {
329 // We are on the leftmost column, wrap
340 * Increment the cursor by one. If at the last column on the last line,
343 * @return true if the cursor position changed
345 public boolean right() {
346 if (!lines
.get(lineNumber
).right()) {
347 // We are on the rightmost column, wrap
358 * Go back to the beginning of this word if in the middle, or the
359 * beginning of the previous word.
361 public void backwardsWord() {
363 // If at the beginning of a word already, push past it.
364 if ((getChar() != -1)
365 && (getRawLine().length() > 0)
366 && !Character
.isWhitespace((char) getChar())
371 // int line = lineNumber;
372 while ((getChar() == -1)
373 || (getRawLine().length() == 0)
374 || Character
.isWhitespace((char) getChar())
376 if (left() == false) {
382 assert (getChar() != -1);
384 if (!Character
.isWhitespace((char) getChar())
385 && (getRawLine().length() > 0)
387 // Advance until at the beginning of the document or a whitespace
389 while (!Character
.isWhitespace((char) getChar())) {
390 int line
= lineNumber
;
391 if (left() == false) {
392 // End of document, bail out.
395 if (lineNumber
!= line
) {
396 // We wrapped a line. Here that counts as whitespace.
403 // We went one past the word, push back to the first character of
410 * Go to the beginning of the next word.
412 public void forwardsWord() {
413 int line
= lineNumber
;
414 while ((getChar() == -1)
415 || (getRawLine().length() == 0)
417 if (right() == false) {
420 if (lineNumber
!= line
) {
421 // We wrapped a line. Here that counts as whitespace.
422 if (!Character
.isWhitespace((char) getChar())) {
423 // We found a character immediately after the line.
431 assert (getChar() != -1);
433 if (!Character
.isWhitespace((char) getChar())
434 && (getRawLine().length() > 0)
436 // Advance until at the end of the document or a whitespace is
438 while (!Character
.isWhitespace((char) getChar())) {
440 if (right() == false) {
441 // End of document, bail out.
444 if (lineNumber
!= line
) {
445 // We wrapped a line. Here that counts as whitespace.
446 if (!Character
.isWhitespace((char) getChar())
447 && (getRawLine().length() > 0)
449 // We found a character immediately after the line.
458 while ((getChar() == -1)
459 || (getRawLine().length() == 0)
461 if (right() == false) {
464 if (lineNumber
!= line
) {
465 // We wrapped a line. Here that counts as whitespace.
466 if (!Character
.isWhitespace((char) getChar())) {
467 // We found a character immediately after the line.
475 assert (getChar() != -1);
477 if (Character
.isWhitespace((char) getChar())) {
478 // Advance until at the end of the document or a non-whitespace
480 while (Character
.isWhitespace((char) getChar())) {
481 if (right() == false) {
482 // End of document, bail out.
489 // We wrapped the line to get here.
494 * Get the raw string that matches this line.
498 public String
getRawLine() {
499 return lines
.get(lineNumber
).getRawString();
503 * Go to the first column of this line.
505 * @return true if the cursor position changed
507 public boolean home() {
508 return lines
.get(lineNumber
).home();
512 * Go to the last column of this line.
514 * @return true if the cursor position changed
516 public boolean end() {
517 return lines
.get(lineNumber
).end();
521 * Delete the character under the cursor.
525 int cursor
= lines
.get(lineNumber
).getCursor();
526 if (cursor
< lines
.get(lineNumber
).getDisplayLength() - 1) {
527 lines
.get(lineNumber
).del();
528 } else if (lineNumber
< lines
.size() - 2) {
530 StringBuilder newLine
= new StringBuilder(lines
.
531 get(lineNumber
).getRawString());
532 newLine
.append(lines
.get(lineNumber
+ 1).getRawString());
533 lines
.set(lineNumber
, new Line(newLine
.toString(),
534 defaultColor
, highlighter
));
535 lines
.get(lineNumber
).setCursor(cursor
);
536 lines
.remove(lineNumber
+ 1);
541 * Delete the character immediately preceeding the cursor.
543 public void backspace() {
545 int cursor
= lines
.get(lineNumber
).getCursor();
547 lines
.get(lineNumber
).backspace();
548 } else if (lineNumber
> 0) {
551 String firstLine
= lines
.get(lineNumber
).getRawString();
552 if (firstLine
.length() > 0) {
553 // Backspacing combining two lines
554 StringBuilder newLine
= new StringBuilder(firstLine
);
555 newLine
.append(lines
.get(lineNumber
+ 1).getRawString());
556 lines
.set(lineNumber
, new Line(newLine
.toString(),
557 defaultColor
, highlighter
));
558 lines
.get(lineNumber
).setCursor(firstLine
.length());
559 lines
.remove(lineNumber
+ 1);
561 // Backspacing an empty line
562 lines
.remove(lineNumber
);
563 lines
.get(lineNumber
).setCursor(0);
569 * Split the current line into two, like pressing the enter key.
571 public void enter() {
573 int cursor
= lines
.get(lineNumber
).getRawCursor();
574 String original
= lines
.get(lineNumber
).getRawString();
575 String firstLine
= original
.substring(0, cursor
);
576 String secondLine
= original
.substring(cursor
);
577 lines
.add(lineNumber
+ 1, new Line(secondLine
, defaultColor
,
579 lines
.set(lineNumber
, new Line(firstLine
, defaultColor
, highlighter
));
581 lines
.get(lineNumber
).home();
585 * Replace or insert a character at the cursor, depending on overwrite
588 * @param ch the character to replace or insert
590 public void addChar(final int ch
) {
593 lines
.get(lineNumber
).replaceChar(ch
);
595 lines
.get(lineNumber
).addChar(ch
);
600 * Get a (shallow) copy of the list of lines.
602 * @return the list of lines
604 public List
<Line
> getLines() {
605 return new ArrayList
<Line
>(lines
);
609 * Get the number of lines.
611 * @return the number of lines
613 public int getLineCount() {
618 * Compute the maximum line length for this document.
620 * @return the number of cells needed to display the longest line
622 public int getLineLengthMax() {
624 for (Line line
: lines
) {
625 if (line
.getDisplayLength() > n
) {
626 n
= line
.getDisplayLength();
633 * Get the current line length.
635 * @return the number of cells needed to display the current line
637 public int getLineLength() {
638 return lines
.get(lineNumber
).getDisplayLength();