Fix bounds check
[nikiroo-utils.git] / src / jexer / teditor / Document.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2017 Kevin Lamonte
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 */
29 package jexer.teditor;
30
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;
36
37 import jexer.bits.CellAttributes;
38
39 /**
40 * A Document represents a text file, as a collection of lines.
41 */
42 public class Document {
43
44 /**
45 * The list of lines.
46 */
47 private ArrayList<Line> lines = new ArrayList<Line>();
48
49 /**
50 * The current line number being edited. Note that this is 0-based, the
51 * first line is line number 0.
52 */
53 private int lineNumber = 0;
54
55 /**
56 * The overwrite flag. When true, characters overwrite data.
57 */
58 private boolean overwrite = false;
59
60 /**
61 * If true, the document has been edited.
62 */
63 private boolean dirty = false;
64
65 /**
66 * The default color for the TEditor class.
67 */
68 private CellAttributes defaultColor = null;
69
70 /**
71 * The text highlighter to use.
72 */
73 private Highlighter highlighter = new Highlighter();
74
75 /**
76 * Get the overwrite flag.
77 *
78 * @return true if addChar() overwrites data, false if it inserts
79 */
80 public boolean getOverwrite() {
81 return overwrite;
82 }
83
84 /**
85 * Get the dirty value.
86 *
87 * @return true if the buffer is dirty
88 */
89 public boolean isDirty() {
90 return dirty;
91 }
92
93 /**
94 * Save contents to file.
95 *
96 * @param filename file to save to
97 * @throws IOException if a java.io operation throws
98 */
99 public void saveToFilename(final String filename) throws IOException {
100 OutputStreamWriter output = null;
101 try {
102 output = new OutputStreamWriter(new FileOutputStream(filename),
103 "UTF-8");
104
105 for (Line line: lines) {
106 output.write(line.getRawString());
107 output.write("\n");
108 }
109
110 dirty = false;
111 }
112 finally {
113 if (output != null) {
114 output.close();
115 }
116 }
117 }
118
119 /**
120 * Set the overwrite flag.
121 *
122 * @param overwrite true if addChar() should overwrite data, false if it
123 * should insert
124 */
125 public void setOverwrite(final boolean overwrite) {
126 this.overwrite = overwrite;
127 }
128
129 /**
130 * Get the current line number being edited.
131 *
132 * @return the line number. Note that this is 0-based: 0 is the first
133 * line.
134 */
135 public int getLineNumber() {
136 return lineNumber;
137 }
138
139 /**
140 * Get the current editing line.
141 *
142 * @return the line
143 */
144 public Line getCurrentLine() {
145 return lines.get(lineNumber);
146 }
147
148 /**
149 * Get a specific line by number.
150 *
151 * @param lineNumber the line number. Note that this is 0-based: 0 is
152 * the first line.
153 * @return the line
154 */
155 public Line getLine(final int lineNumber) {
156 return lines.get(lineNumber);
157 }
158
159 /**
160 * Set the current line number being edited.
161 *
162 * @param n the line number. Note that this is 0-based: 0 is the first
163 * line.
164 */
165 public void setLineNumber(final int n) {
166 if ((n < 0) || (n > lines.size())) {
167 throw new IndexOutOfBoundsException("Lines array size is " +
168 lines.size() + ", requested index " + n);
169 }
170 lineNumber = n;
171 }
172
173 /**
174 * Get the current cursor position of the editing line.
175 *
176 * @return the cursor position
177 */
178 public int getCursor() {
179 return lines.get(lineNumber).getCursor();
180 }
181
182 /**
183 * Set the current cursor position of the editing line. 0-based.
184 *
185 * @param cursor the new cursor position
186 */
187 public void setCursor(final int cursor) {
188 lines.get(lineNumber).setCursor(cursor);
189 }
190
191 /**
192 * Construct a new Document from an existing text string.
193 *
194 * @param str the text string
195 * @param defaultColor the color for unhighlighted text
196 */
197 public Document(final String str, final CellAttributes defaultColor) {
198 this.defaultColor = defaultColor;
199
200 // TODO: set different colors based on file extension
201 highlighter.setJavaColors();
202
203 String [] rawLines = str.split("\n");
204 for (int i = 0; i < rawLines.length; i++) {
205 lines.add(new Line(rawLines[i], this.defaultColor, highlighter));
206 }
207 }
208
209 /**
210 * Increment the line number by one. If at the last line, do nothing.
211 *
212 * @return true if the editing line changed
213 */
214 public boolean down() {
215 if (lineNumber < lines.size() - 1) {
216 int x = lines.get(lineNumber).getCursor();
217 lineNumber++;
218 if (x > lines.get(lineNumber).getDisplayLength()) {
219 lines.get(lineNumber).end();
220 } else {
221 lines.get(lineNumber).setCursor(x);
222 }
223 return true;
224 }
225 return false;
226 }
227
228 /**
229 * Increment the line number by n. If n would go past the last line,
230 * increment only to the last line.
231 *
232 * @param n the number of lines to increment by
233 * @return true if the editing line changed
234 */
235 public boolean down(final int n) {
236 if (lineNumber < lines.size() - 1) {
237 int x = lines.get(lineNumber).getCursor();
238 lineNumber += n;
239 if (lineNumber > lines.size() - 1) {
240 lineNumber = lines.size() - 1;
241 }
242 if (x > lines.get(lineNumber).getDisplayLength()) {
243 lines.get(lineNumber).end();
244 } else {
245 lines.get(lineNumber).setCursor(x);
246 }
247 return true;
248 }
249 return false;
250 }
251
252 /**
253 * Decrement the line number by one. If at the first line, do nothing.
254 *
255 * @return true if the editing line changed
256 */
257 public boolean up() {
258 if (lineNumber > 0) {
259 int x = lines.get(lineNumber).getCursor();
260 lineNumber--;
261 if (x > lines.get(lineNumber).getDisplayLength()) {
262 lines.get(lineNumber).end();
263 } else {
264 lines.get(lineNumber).setCursor(x);
265 }
266 return true;
267 }
268 return false;
269 }
270
271 /**
272 * Decrement the line number by n. If n would go past the first line,
273 * decrement only to the first line.
274 *
275 * @param n the number of lines to decrement by
276 * @return true if the editing line changed
277 */
278 public boolean up(final int n) {
279 if (lineNumber > 0) {
280 int x = lines.get(lineNumber).getCursor();
281 lineNumber -= n;
282 if (lineNumber < 0) {
283 lineNumber = 0;
284 }
285 if (x > lines.get(lineNumber).getDisplayLength()) {
286 lines.get(lineNumber).end();
287 } else {
288 lines.get(lineNumber).setCursor(x);
289 }
290 return true;
291 }
292 return false;
293 }
294
295 /**
296 * Decrement the cursor by one. If at the first column, do nothing.
297 *
298 * @return true if the cursor position changed
299 */
300 public boolean left() {
301 if (!lines.get(lineNumber).left()) {
302 // We are on the leftmost column, wrap
303 if (up()) {
304 end();
305 } else {
306 return false;
307 }
308 }
309 return true;
310 }
311
312 /**
313 * Increment the cursor by one. If at the last column, do nothing.
314 *
315 * @return true if the cursor position changed
316 */
317 public boolean right() {
318 if (!lines.get(lineNumber).right()) {
319 // We are on the rightmost column, wrap
320 if (down()) {
321 home();
322 } else {
323 return false;
324 }
325 }
326 return true;
327 }
328
329 /**
330 * Go to the first column of this line.
331 *
332 * @return true if the cursor position changed
333 */
334 public boolean home() {
335 return lines.get(lineNumber).home();
336 }
337
338 /**
339 * Go to the last column of this line.
340 *
341 * @return true if the cursor position changed
342 */
343 public boolean end() {
344 return lines.get(lineNumber).end();
345 }
346
347 /**
348 * Delete the character under the cursor.
349 */
350 public void del() {
351 dirty = true;
352 int cursor = lines.get(lineNumber).getCursor();
353 if (cursor < lines.get(lineNumber).getDisplayLength() - 1) {
354 lines.get(lineNumber).del();
355 } else if (lineNumber < lines.size() - 2) {
356 // Join two lines
357 StringBuilder newLine = new StringBuilder(lines.
358 get(lineNumber).getRawString());
359 newLine.append(lines.get(lineNumber + 1).getRawString());
360 lines.set(lineNumber, new Line(newLine.toString(),
361 defaultColor, highlighter));
362 lines.get(lineNumber).setCursor(cursor);
363 lines.remove(lineNumber + 1);
364 }
365 }
366
367 /**
368 * Delete the character immediately preceeding the cursor.
369 */
370 public void backspace() {
371 dirty = true;
372 int cursor = lines.get(lineNumber).getCursor();
373 if (cursor > 0) {
374 lines.get(lineNumber).backspace();
375 } else if (lineNumber > 0) {
376 // Join two lines
377 lineNumber--;
378 String firstLine = lines.get(lineNumber).getRawString();
379 if (firstLine.length() > 0) {
380 // Backspacing combining two lines
381 StringBuilder newLine = new StringBuilder(firstLine);
382 newLine.append(lines.get(lineNumber + 1).getRawString());
383 lines.set(lineNumber, new Line(newLine.toString(),
384 defaultColor, highlighter));
385 lines.get(lineNumber).setCursor(firstLine.length());
386 lines.remove(lineNumber + 1);
387 } else {
388 // Backspacing an empty line
389 lines.remove(lineNumber);
390 lines.get(lineNumber).setCursor(0);
391 }
392 }
393 }
394
395 /**
396 * Split the current line into two, like pressing the enter key.
397 */
398 public void enter() {
399 dirty = true;
400 int cursor = lines.get(lineNumber).getCursor();
401 String original = lines.get(lineNumber).getRawString();
402 String firstLine = original.substring(0, cursor);
403 String secondLine = original.substring(cursor);
404 lines.add(lineNumber + 1, new Line(secondLine, defaultColor,
405 highlighter));
406 lines.set(lineNumber, new Line(firstLine, defaultColor, highlighter));
407 lineNumber++;
408 lines.get(lineNumber).home();
409 }
410
411 /**
412 * Replace or insert a character at the cursor, depending on overwrite
413 * flag.
414 *
415 * @param ch the character to replace or insert
416 */
417 public void addChar(final char ch) {
418 dirty = true;
419 if (overwrite) {
420 lines.get(lineNumber).replaceChar(ch);
421 } else {
422 lines.get(lineNumber).addChar(ch);
423 }
424 }
425
426 /**
427 * Get a (shallow) copy of the list of lines.
428 *
429 * @return the list of lines
430 */
431 public List<Line> getLines() {
432 return new ArrayList<Line>(lines);
433 }
434
435 /**
436 * Get the number of lines.
437 *
438 * @return the number of lines
439 */
440 public int getLineCount() {
441 return lines.size();
442 }
443
444 /**
445 * Compute the maximum line length for this document.
446 *
447 * @return the number of cells needed to display the longest line
448 */
449 public int getLineLengthMax() {
450 int n = 0;
451 for (Line line : lines) {
452 if (line.getDisplayLength() > n) {
453 n = line.getDisplayLength();
454 }
455 }
456 return n;
457 }
458
459 /**
460 * Get the current line length.
461 *
462 * @return the number of cells needed to display the current line
463 */
464 public int getLineLength() {
465 return lines.get(lineNumber).getDisplayLength();
466 }
467
468 }