fix double-width/height on xterm backend
[fanfix.git] / src / jexer / teditor / Document.java
CommitLineData
12b55d76
KL
1/*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
a69ed767 6 * Copyright (C) 2019 Kevin Lamonte
12b55d76
KL
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 */
29package jexer.teditor;
30
71a389c9
KL
31import java.io.FileOutputStream;
32import java.io.IOException;
33import java.io.OutputStreamWriter;
12b55d76
KL
34import java.util.ArrayList;
35import java.util.List;
36
e8a11f98
KL
37import jexer.bits.CellAttributes;
38
12b55d76
KL
39/**
40 * A Document represents a text file, as a collection of lines.
41 */
42public class Document {
43
615a0d99
KL
44 // ------------------------------------------------------------------------
45 // Variables --------------------------------------------------------------
46 // ------------------------------------------------------------------------
47
12b55d76
KL
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
71a389c9
KL
64 /**
65 * If true, the document has been edited.
66 */
67 private boolean dirty = false;
68
e8a11f98
KL
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
615a0d99
KL
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
12b55d76
KL
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
71a389c9
KL
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
12b55d76
KL
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
e8a11f98
KL
169 /**
170 * Get the current editing line.
171 *
172 * @return the line
173 */
174 public Line getCurrentLine() {
175 return lines.get(lineNumber);
176 }
177
12b55d76
KL
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())) {
e8a11f98
KL
197 throw new IndexOutOfBoundsException("Lines array size is " +
198 lines.size() + ", requested index " + n);
12b55d76
KL
199 }
200 lineNumber = n;
201 }
202
e8a11f98
KL
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
71a389c9
KL
212 /**
213 * Set the current cursor position of the editing line. 0-based.
214 *
215 * @param cursor the new cursor position
216 */
217 public void setCursor(final int cursor) {
4297b49b
KL
218 if (cursor >= lines.get(lineNumber).getDisplayLength()) {
219 lines.get(lineNumber).end();
220 } else {
221 lines.get(lineNumber).setCursor(cursor);
222 }
71a389c9
KL
223 }
224
12b55d76
KL
225 /**
226 * Increment the line number by one. If at the last line, do nothing.
e8a11f98
KL
227 *
228 * @return true if the editing line changed
12b55d76 229 */
e8a11f98 230 public boolean down() {
12b55d76 231 if (lineNumber < lines.size() - 1) {
e8a11f98 232 int x = lines.get(lineNumber).getCursor();
12b55d76 233 lineNumber++;
4297b49b 234 if (x >= lines.get(lineNumber).getDisplayLength()) {
e8a11f98
KL
235 lines.get(lineNumber).end();
236 } else {
237 lines.get(lineNumber).setCursor(x);
238 }
239 return true;
12b55d76 240 }
e8a11f98 241 return false;
12b55d76
KL
242 }
243
244 /**
245 * Increment the line number by n. If n would go past the last line,
246 * increment only to the last line.
247 *
248 * @param n the number of lines to increment by
e8a11f98 249 * @return true if the editing line changed
12b55d76 250 */
e8a11f98
KL
251 public boolean down(final int n) {
252 if (lineNumber < lines.size() - 1) {
253 int x = lines.get(lineNumber).getCursor();
254 lineNumber += n;
255 if (lineNumber > lines.size() - 1) {
256 lineNumber = lines.size() - 1;
257 }
4297b49b 258 if (x >= lines.get(lineNumber).getDisplayLength()) {
e8a11f98
KL
259 lines.get(lineNumber).end();
260 } else {
261 lines.get(lineNumber).setCursor(x);
262 }
263 return true;
12b55d76 264 }
e8a11f98 265 return false;
12b55d76
KL
266 }
267
268 /**
269 * Decrement the line number by one. If at the first line, do nothing.
e8a11f98
KL
270 *
271 * @return true if the editing line changed
12b55d76 272 */
e8a11f98 273 public boolean up() {
12b55d76 274 if (lineNumber > 0) {
e8a11f98 275 int x = lines.get(lineNumber).getCursor();
12b55d76 276 lineNumber--;
4297b49b 277 if (x >= lines.get(lineNumber).getDisplayLength()) {
e8a11f98
KL
278 lines.get(lineNumber).end();
279 } else {
280 lines.get(lineNumber).setCursor(x);
281 }
282 return true;
12b55d76 283 }
e8a11f98 284 return false;
12b55d76
KL
285 }
286
287 /**
288 * Decrement the line number by n. If n would go past the first line,
289 * decrement only to the first line.
290 *
291 * @param n the number of lines to decrement by
e8a11f98 292 * @return true if the editing line changed
12b55d76 293 */
e8a11f98
KL
294 public boolean up(final int n) {
295 if (lineNumber > 0) {
296 int x = lines.get(lineNumber).getCursor();
297 lineNumber -= n;
298 if (lineNumber < 0) {
299 lineNumber = 0;
300 }
4297b49b 301 if (x >= lines.get(lineNumber).getDisplayLength()) {
e8a11f98
KL
302 lines.get(lineNumber).end();
303 } else {
304 lines.get(lineNumber).setCursor(x);
305 }
306 return true;
12b55d76 307 }
e8a11f98 308 return false;
12b55d76
KL
309 }
310
311 /**
312 * Decrement the cursor by one. If at the first column, do nothing.
e8a11f98
KL
313 *
314 * @return true if the cursor position changed
12b55d76 315 */
e8a11f98 316 public boolean left() {
df602ccf
KL
317 if (!lines.get(lineNumber).left()) {
318 // We are on the leftmost column, wrap
319 if (up()) {
320 end();
321 } else {
322 return false;
323 }
324 }
325 return true;
12b55d76
KL
326 }
327
328 /**
329 * Increment the cursor by one. If at the last column, do nothing.
e8a11f98
KL
330 *
331 * @return true if the cursor position changed
12b55d76 332 */
e8a11f98 333 public boolean right() {
df602ccf
KL
334 if (!lines.get(lineNumber).right()) {
335 // We are on the rightmost column, wrap
336 if (down()) {
337 home();
338 } else {
339 return false;
340 }
341 }
342 return true;
12b55d76
KL
343 }
344
345 /**
346 * Go to the first column of this line.
e8a11f98
KL
347 *
348 * @return true if the cursor position changed
12b55d76 349 */
e8a11f98
KL
350 public boolean home() {
351 return lines.get(lineNumber).home();
12b55d76
KL
352 }
353
354 /**
355 * Go to the last column of this line.
e8a11f98
KL
356 *
357 * @return true if the cursor position changed
12b55d76 358 */
e8a11f98
KL
359 public boolean end() {
360 return lines.get(lineNumber).end();
12b55d76
KL
361 }
362
363 /**
364 * Delete the character under the cursor.
365 */
366 public void del() {
71a389c9 367 dirty = true;
df602ccf
KL
368 int cursor = lines.get(lineNumber).getCursor();
369 if (cursor < lines.get(lineNumber).getDisplayLength() - 1) {
370 lines.get(lineNumber).del();
371 } else if (lineNumber < lines.size() - 2) {
372 // Join two lines
373 StringBuilder newLine = new StringBuilder(lines.
374 get(lineNumber).getRawString());
375 newLine.append(lines.get(lineNumber + 1).getRawString());
376 lines.set(lineNumber, new Line(newLine.toString(),
377 defaultColor, highlighter));
378 lines.get(lineNumber).setCursor(cursor);
379 lines.remove(lineNumber + 1);
380 }
12b55d76
KL
381 }
382
383 /**
384 * Delete the character immediately preceeding the cursor.
385 */
386 public void backspace() {
71a389c9 387 dirty = true;
df602ccf
KL
388 int cursor = lines.get(lineNumber).getCursor();
389 if (cursor > 0) {
390 lines.get(lineNumber).backspace();
391 } else if (lineNumber > 0) {
392 // Join two lines
393 lineNumber--;
394 String firstLine = lines.get(lineNumber).getRawString();
395 if (firstLine.length() > 0) {
396 // Backspacing combining two lines
397 StringBuilder newLine = new StringBuilder(firstLine);
398 newLine.append(lines.get(lineNumber + 1).getRawString());
399 lines.set(lineNumber, new Line(newLine.toString(),
400 defaultColor, highlighter));
401 lines.get(lineNumber).setCursor(firstLine.length());
402 lines.remove(lineNumber + 1);
403 } else {
404 // Backspacing an empty line
405 lines.remove(lineNumber);
406 lines.get(lineNumber).setCursor(0);
407 }
408 }
409 }
410
411 /**
412 * Split the current line into two, like pressing the enter key.
413 */
414 public void enter() {
415 dirty = true;
416 int cursor = lines.get(lineNumber).getCursor();
417 String original = lines.get(lineNumber).getRawString();
418 String firstLine = original.substring(0, cursor);
419 String secondLine = original.substring(cursor);
420 lines.add(lineNumber + 1, new Line(secondLine, defaultColor,
421 highlighter));
422 lines.set(lineNumber, new Line(firstLine, defaultColor, highlighter));
423 lineNumber++;
424 lines.get(lineNumber).home();
12b55d76
KL
425 }
426
427 /**
428 * Replace or insert a character at the cursor, depending on overwrite
429 * flag.
430 *
431 * @param ch the character to replace or insert
432 */
433 public void addChar(final char ch) {
71a389c9 434 dirty = true;
e8a11f98
KL
435 if (overwrite) {
436 lines.get(lineNumber).replaceChar(ch);
437 } else {
438 lines.get(lineNumber).addChar(ch);
439 }
12b55d76
KL
440 }
441
442 /**
443 * Get a (shallow) copy of the list of lines.
444 *
445 * @return the list of lines
446 */
447 public List<Line> getLines() {
448 return new ArrayList<Line>(lines);
449 }
450
451 /**
452 * Get the number of lines.
453 *
454 * @return the number of lines
455 */
456 public int getLineCount() {
457 return lines.size();
458 }
459
460 /**
461 * Compute the maximum line length for this document.
462 *
463 * @return the number of cells needed to display the longest line
464 */
465 public int getLineLengthMax() {
466 int n = 0;
467 for (Line line : lines) {
468 if (line.getDisplayLength() > n) {
469 n = line.getDisplayLength();
470 }
471 }
472 return n;
473 }
474
fe0770f9
KL
475 /**
476 * Get the current line length.
477 *
478 * @return the number of cells needed to display the current line
479 */
480 public int getLineLength() {
481 return lines.get(lineNumber).getDisplayLength();
482 }
483
12b55d76 484}