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();
82 private int tabSize
= 8;
85 * If true, backspace at an indent level goes back a full indent level.
86 * If false, backspace always goes back one column.
88 private boolean backspaceUnindents
= false;
91 * If true, save files with tab characters. If false, convert tabs to
92 * spaces when saving files.
94 private boolean saveWithTabs
= false;
96 // ------------------------------------------------------------------------
97 // Constructors -----------------------------------------------------------
98 // ------------------------------------------------------------------------
101 * Construct a new Document from an existing text string.
103 * @param str the text string
104 * @param defaultColor the color for unhighlighted text
106 public Document(final String str
, final CellAttributes defaultColor
) {
107 this.defaultColor
= defaultColor
;
109 // Set colors to resemble the Borland IDE colors, but for Java
110 // language keywords.
111 highlighter
.setJavaColors();
113 String
[] rawLines
= str
.split("\n");
114 for (int i
= 0; i
< rawLines
.length
; i
++) {
115 lines
.add(new Line(rawLines
[i
], this.defaultColor
, highlighter
));
120 * Private constructor used by dup().
126 // ------------------------------------------------------------------------
127 // Document ---------------------------------------------------------------
128 // ------------------------------------------------------------------------
131 * Create a duplicate instance.
133 * @return duplicate intance
135 public Document
dup() {
136 Document other
= new Document();
137 for (Line line
: lines
) {
138 other
.lines
.add(line
.dup());
140 other
.lineNumber
= lineNumber
;
141 other
.overwrite
= overwrite
;
143 other
.defaultColor
= defaultColor
;
144 other
.highlighter
.setTo(highlighter
);
149 * Get the overwrite flag.
151 * @return true if addChar() overwrites data, false if it inserts
153 public boolean isOverwrite() {
158 * Get the dirty value.
160 * @return true if the buffer is dirty
162 public boolean isDirty() {
167 * Unset the dirty flag.
169 public void setNotDirty() {
174 * Save contents to file.
176 * @param filename file to save to
177 * @throws IOException if a java.io operation throws
179 public void saveToFilename(final String filename
) throws IOException
{
180 OutputStreamWriter output
= null;
182 output
= new OutputStreamWriter(new FileOutputStream(filename
),
185 for (Line line
: lines
) {
187 output
.write(convertSpacesToTabs(line
.getRawString()));
189 output
.write(line
.getRawString());
197 if (output
!= null) {
204 * Set the overwrite flag.
206 * @param overwrite true if addChar() should overwrite data, false if it
209 public void setOverwrite(final boolean overwrite
) {
210 this.overwrite
= overwrite
;
214 * Get the current line number being edited.
216 * @return the line number. Note that this is 0-based: 0 is the first
219 public int getLineNumber() {
224 * Get the current editing line.
228 public Line
getCurrentLine() {
229 return lines
.get(lineNumber
);
233 * Get a specific line by number.
235 * @param lineNumber the line number. Note that this is 0-based: 0 is
239 public Line
getLine(final int lineNumber
) {
240 return lines
.get(lineNumber
);
244 * Set the current line number being edited.
246 * @param n the line number. Note that this is 0-based: 0 is the first
249 public void setLineNumber(final int n
) {
250 if ((n
< 0) || (n
> lines
.size())) {
251 throw new IndexOutOfBoundsException("Lines array size is " +
252 lines
.size() + ", requested index " + n
);
258 * Get the current cursor position of the editing line.
260 * @return the cursor position
262 public int getCursor() {
263 return lines
.get(lineNumber
).getCursor();
267 * Get the character at the current cursor position in the text.
269 * @return the character, or -1 if the cursor is at the end of the line
271 public int getChar() {
272 return lines
.get(lineNumber
).getChar();
276 * Set the current cursor position of the editing line. 0-based.
278 * @param cursor the new cursor position
280 public void setCursor(final int cursor
) {
281 if (cursor
>= lines
.get(lineNumber
).getDisplayLength()) {
282 lines
.get(lineNumber
).end();
284 lines
.get(lineNumber
).setCursor(cursor
);
289 * Increment the line number by one. If at the last line, do nothing.
291 * @return true if the editing line changed
293 public boolean down() {
294 if (lineNumber
< lines
.size() - 1) {
295 int x
= lines
.get(lineNumber
).getCursor();
297 if (x
>= lines
.get(lineNumber
).getDisplayLength()) {
298 lines
.get(lineNumber
).end();
300 lines
.get(lineNumber
).setCursor(x
);
308 * Increment the line number by n. If n would go past the last line,
309 * increment only to the last line.
311 * @param n the number of lines to increment by
312 * @return true if the editing line changed
314 public boolean down(final int n
) {
315 if (lineNumber
< lines
.size() - 1) {
316 int x
= lines
.get(lineNumber
).getCursor();
318 if (lineNumber
> lines
.size() - 1) {
319 lineNumber
= lines
.size() - 1;
321 if (x
>= lines
.get(lineNumber
).getDisplayLength()) {
322 lines
.get(lineNumber
).end();
324 lines
.get(lineNumber
).setCursor(x
);
332 * Decrement the line number by one. If at the first line, do nothing.
334 * @return true if the editing line changed
336 public boolean up() {
337 if (lineNumber
> 0) {
338 int x
= lines
.get(lineNumber
).getCursor();
340 if (x
>= lines
.get(lineNumber
).getDisplayLength()) {
341 lines
.get(lineNumber
).end();
343 lines
.get(lineNumber
).setCursor(x
);
351 * Decrement the line number by n. If n would go past the first line,
352 * decrement only to the first line.
354 * @param n the number of lines to decrement by
355 * @return true if the editing line changed
357 public boolean up(final int n
) {
358 if (lineNumber
> 0) {
359 int x
= lines
.get(lineNumber
).getCursor();
361 if (lineNumber
< 0) {
364 if (x
>= lines
.get(lineNumber
).getDisplayLength()) {
365 lines
.get(lineNumber
).end();
367 lines
.get(lineNumber
).setCursor(x
);
375 * Decrement the cursor by one. If at the first column on the first
378 * @return true if the cursor position changed
380 public boolean left() {
381 if (!lines
.get(lineNumber
).left()) {
382 // We are on the leftmost column, wrap
393 * Increment the cursor by one. If at the last column on the last line,
396 * @return true if the cursor position changed
398 public boolean right() {
399 if (!lines
.get(lineNumber
).right()) {
400 // We are on the rightmost column, wrap
411 * Go back to the beginning of this word if in the middle, or the
412 * beginning of the previous word.
414 public void backwardsWord() {
416 // If at the beginning of a word already, push past it.
417 if ((getChar() != -1)
418 && (getRawLine().length() > 0)
419 && !Character
.isWhitespace((char) getChar())
424 // int line = lineNumber;
425 while ((getChar() == -1)
426 || (getRawLine().length() == 0)
427 || Character
.isWhitespace((char) getChar())
429 if (left() == false) {
435 assert (getChar() != -1);
437 if (!Character
.isWhitespace((char) getChar())
438 && (getRawLine().length() > 0)
440 // Advance until at the beginning of the document or a whitespace
442 while (!Character
.isWhitespace((char) getChar())) {
443 int line
= lineNumber
;
444 if (left() == false) {
445 // End of document, bail out.
448 if (lineNumber
!= line
) {
449 // We wrapped a line. Here that counts as whitespace.
456 // We went one past the word, push back to the first character of
463 * Go to the beginning of the next word.
465 public void forwardsWord() {
466 int line
= lineNumber
;
467 while ((getChar() == -1)
468 || (getRawLine().length() == 0)
470 if (right() == false) {
473 if (lineNumber
!= line
) {
474 // We wrapped a line. Here that counts as whitespace.
475 if (!Character
.isWhitespace((char) getChar())) {
476 // We found a character immediately after the line.
484 assert (getChar() != -1);
486 if (!Character
.isWhitespace((char) getChar())
487 && (getRawLine().length() > 0)
489 // Advance until at the end of the document or a whitespace is
491 while (!Character
.isWhitespace((char) getChar())) {
493 if (right() == false) {
494 // End of document, bail out.
497 if (lineNumber
!= line
) {
498 // We wrapped a line. Here that counts as whitespace.
499 if (!Character
.isWhitespace((char) getChar())
500 && (getRawLine().length() > 0)
502 // We found a character immediately after the line.
511 while ((getChar() == -1)
512 || (getRawLine().length() == 0)
514 if (right() == false) {
517 if (lineNumber
!= line
) {
518 // We wrapped a line. Here that counts as whitespace.
519 if (!Character
.isWhitespace((char) getChar())) {
520 // We found a character immediately after the line.
528 assert (getChar() != -1);
530 if (Character
.isWhitespace((char) getChar())) {
531 // Advance until at the end of the document or a non-whitespace
533 while (Character
.isWhitespace((char) getChar())) {
534 if (right() == false) {
535 // End of document, bail out.
542 // We wrapped the line to get here.
547 * Get the raw string that matches this line.
551 public String
getRawLine() {
552 return lines
.get(lineNumber
).getRawString();
556 * Go to the first column of this line.
558 * @return true if the cursor position changed
560 public boolean home() {
561 return lines
.get(lineNumber
).home();
565 * Go to the last column of this line.
567 * @return true if the cursor position changed
569 public boolean end() {
570 return lines
.get(lineNumber
).end();
574 * Delete the character under the cursor.
578 int cursor
= lines
.get(lineNumber
).getCursor();
579 if (cursor
< lines
.get(lineNumber
).getDisplayLength() - 1) {
580 lines
.get(lineNumber
).del();
581 } else if (lineNumber
< lines
.size() - 2) {
583 StringBuilder newLine
= new StringBuilder(lines
.
584 get(lineNumber
).getRawString());
585 newLine
.append(lines
.get(lineNumber
+ 1).getRawString());
586 lines
.set(lineNumber
, new Line(newLine
.toString(),
587 defaultColor
, highlighter
));
588 lines
.get(lineNumber
).setCursor(cursor
);
589 lines
.remove(lineNumber
+ 1);
594 * Delete the character immediately preceeding the cursor.
596 public void backspace() {
598 int cursor
= lines
.get(lineNumber
).getCursor();
600 lines
.get(lineNumber
).backspace(tabSize
, backspaceUnindents
);
601 } else if (lineNumber
> 0) {
604 String firstLine
= lines
.get(lineNumber
).getRawString();
605 if (firstLine
.length() > 0) {
606 // Backspacing combining two lines
607 StringBuilder newLine
= new StringBuilder(firstLine
);
608 newLine
.append(lines
.get(lineNumber
+ 1).getRawString());
609 lines
.set(lineNumber
, new Line(newLine
.toString(),
610 defaultColor
, highlighter
));
611 lines
.get(lineNumber
).setCursor(firstLine
.length());
612 lines
.remove(lineNumber
+ 1);
614 // Backspacing an empty line
615 lines
.remove(lineNumber
);
616 lines
.get(lineNumber
).setCursor(0);
622 * Split the current line into two, like pressing the enter key.
624 public void enter() {
626 int cursor
= lines
.get(lineNumber
).getRawCursor();
627 String original
= lines
.get(lineNumber
).getRawString();
628 String firstLine
= original
.substring(0, cursor
);
629 String secondLine
= original
.substring(cursor
);
630 lines
.add(lineNumber
+ 1, new Line(secondLine
, defaultColor
,
632 lines
.set(lineNumber
, new Line(firstLine
, defaultColor
, highlighter
));
634 lines
.get(lineNumber
).home();
638 * Replace or insert a character at the cursor, depending on overwrite
641 * @param ch the character to replace or insert
643 public void addChar(final int ch
) {
646 lines
.get(lineNumber
).replaceChar(ch
);
648 lines
.get(lineNumber
).addChar(ch
);
653 * Get the tab stop size.
655 * @return the tab stop size
657 public int getTabSize() {
662 * Set the tab stop size.
664 * @param tabSize the new tab stop size
666 public void setTabSize(final int tabSize
) {
667 this.tabSize
= tabSize
;
671 * Set the backspace unindent option.
673 * @param backspaceUnindents If true, backspace at an indent level goes
674 * back a full indent level. If false, backspace always goes back one
677 public void setBackspaceUnindents(final boolean backspaceUnindents
) {
678 this.backspaceUnindents
= backspaceUnindents
;
682 * Set the save with tabs option.
684 * @param saveWithTabs If true, save files with tab characters. If
685 * false, convert tabs to spaces when saving files.
687 public void setSaveWithTabs(final boolean saveWithTabs
) {
688 this.saveWithTabs
= saveWithTabs
;
692 * Handle the tab character.
698 lines
.get(lineNumber
).tab(tabSize
);
702 * Handle the backtab (shift-tab) character.
704 public void backTab() {
705 lines
.get(lineNumber
).backTab(tabSize
);
709 * Get a (shallow) copy of the list of lines.
711 * @return the list of lines
713 public List
<Line
> getLines() {
714 return new ArrayList
<Line
>(lines
);
718 * Get the number of lines.
720 * @return the number of lines
722 public int getLineCount() {
727 * Compute the maximum line length for this document.
729 * @return the number of cells needed to display the longest line
731 public int getLineLengthMax() {
733 for (Line line
: lines
) {
734 if (line
.getDisplayLength() > n
) {
735 n
= line
.getDisplayLength();
742 * Get the current line length.
744 * @return the number of cells needed to display the current line
746 public int getLineLength() {
747 return lines
.get(lineNumber
).getDisplayLength();
751 * Get the entire contents of the document as one string.
753 * @return the document contents
755 public String
getText() {
756 StringBuilder sb
= new StringBuilder();
757 for (Line line
: getLines()) {
758 sb
.append(line
.getRawString());
761 return sb
.toString();
765 * Trim trailing whitespace from lines and trailing empty
766 * lines from the document.
768 public void cleanWhitespace() {
769 for (Line line
: getLines()) {
772 if (lines
.size() == 0) {
775 while (lines
.get(lines
.size() - 1).length() == 0) {
776 lines
.remove(lines
.size() - 1);
778 if (lineNumber
> lines
.size() - 1) {
779 lineNumber
= lines
.size() - 1;
784 * Set keyword highlighting.
786 * @param enabled if true, enable keyword highlighting
788 public void setHighlighting(final boolean enabled
) {
789 highlighter
.setEnabled(enabled
);
790 for (Line line
: getLines()) {
796 * Convert a string with leading spaces to a mix of tabs and spaces.
798 * @param string the string to convert
800 private String
convertSpacesToTabs(final String string
) {
801 if (string
.length() == 0) {
806 while (string
.charAt(start
) == ' ') {
809 int tabCount
= start
/ 8;
814 StringBuilder sb
= new StringBuilder(string
.length());
816 for (int i
= 0; i
< tabCount
; i
++) {
819 sb
.append(string
.substring(tabCount
* 8));
820 return sb
.toString();