Merge branch 'upstream' into subtree
[nikiroo-utils.git] / teditor / Line.java
1 /*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
6 * Copyright (C) 2019 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.util.ArrayList;
32 import java.util.List;
33
34 import jexer.bits.CellAttributes;
35 import jexer.bits.GraphicsChars;
36 import jexer.bits.StringUtils;
37
38 /**
39 * A Line represents a single line of text on the screen, as a collection of
40 * words.
41 */
42 public class Line {
43
44 // ------------------------------------------------------------------------
45 // Variables --------------------------------------------------------------
46 // ------------------------------------------------------------------------
47
48 /**
49 * The list of words.
50 */
51 private ArrayList<Word> words = new ArrayList<Word>();
52
53 /**
54 * The default color for the TEditor class.
55 */
56 private CellAttributes defaultColor = null;
57
58 /**
59 * The text highlighter to use.
60 */
61 private Highlighter highlighter = null;
62
63 /**
64 * The current edition position on this line.
65 */
66 private int position = 0;
67
68 /**
69 * The current editing position screen column number.
70 */
71 private int screenPosition = 0;
72
73 /**
74 * The raw text of this line, what is passed to Word to determine
75 * highlighting behavior.
76 */
77 private StringBuilder rawText;
78
79 // ------------------------------------------------------------------------
80 // Constructors -----------------------------------------------------------
81 // ------------------------------------------------------------------------
82
83 /**
84 * Construct a new Line from an existing text string, and highlight
85 * certain strings.
86 *
87 * @param str the text string
88 * @param defaultColor the color for unhighlighted text
89 * @param highlighter the highlighter to use
90 */
91 public Line(final String str, final CellAttributes defaultColor,
92 final Highlighter highlighter) {
93
94 this.defaultColor = defaultColor;
95 this.highlighter = highlighter;
96
97 this.rawText = new StringBuilder();
98 int col = 0;
99 for (int i = 0; i < str.length(); i++) {
100 char ch = str.charAt(i);
101 if (ch == '\t') {
102 // Expand tabs
103 int j = col % 8;
104 do {
105 rawText.append(' ');
106 j++;
107 col++;
108 } while ((j % 8) != 0);
109 continue;
110 }
111 if ((ch <= 0x20) || (ch == 0x7F)) {
112 // Replace all other C0 bytes with CP437 glyphs.
113 rawText.append(GraphicsChars.CP437[(int) ch]);
114 col++;
115 continue;
116 }
117
118 rawText.append(ch);
119 col++;
120 }
121
122 scanLine();
123 }
124
125 /**
126 * Construct a new Line from an existing text string.
127 *
128 * @param str the text string
129 * @param defaultColor the color for unhighlighted text
130 */
131 public Line(final String str, final CellAttributes defaultColor) {
132 this(str, defaultColor, null);
133 }
134
135 /**
136 * Private constructor used by dup().
137 */
138 private Line() {
139 // NOP
140 }
141
142 // ------------------------------------------------------------------------
143 // Line -------------------------------------------------------------------
144 // ------------------------------------------------------------------------
145
146 /**
147 * Create a duplicate instance.
148 *
149 * @return duplicate intance
150 */
151 public Line dup() {
152 Line other = new Line();
153 other.defaultColor = defaultColor;
154 other.highlighter = highlighter;
155 other.position = position;
156 other.screenPosition = screenPosition;
157 other.rawText = new StringBuilder(rawText);
158 other.scanLine();
159 return other;
160 }
161
162 /**
163 * Get a (shallow) copy of the words in this line.
164 *
165 * @return a copy of the word list
166 */
167 public List<Word> getWords() {
168 return new ArrayList<Word>(words);
169 }
170
171 /**
172 * Get the current cursor position in the text.
173 *
174 * @return the cursor position
175 */
176 public int getRawCursor() {
177 return position;
178 }
179
180 /**
181 * Get the current cursor position on screen.
182 *
183 * @return the cursor position
184 */
185 public int getCursor() {
186 return screenPosition;
187 }
188
189 /**
190 * Set the current cursor position.
191 *
192 * @param cursor the new cursor position
193 */
194 public void setCursor(final int cursor) {
195 if ((cursor < 0)
196 || ((cursor >= getDisplayLength())
197 && (getDisplayLength() > 0))
198 ) {
199 throw new IndexOutOfBoundsException("Max length is " +
200 getDisplayLength() + ", requested position " + cursor);
201 }
202 screenPosition = cursor;
203 position = screenToTextPosition(screenPosition);
204 }
205
206 /**
207 * Get the character at the current cursor position in the text.
208 *
209 * @return the character, or -1 if the cursor is at the end of the line
210 */
211 public int getChar() {
212 if (position == rawText.length()) {
213 return -1;
214 }
215 return rawText.codePointAt(position);
216 }
217
218 /**
219 * Get the on-screen display length.
220 *
221 * @return the number of cells needed to display this line
222 */
223 public int getDisplayLength() {
224 int n = StringUtils.width(rawText.toString());
225
226 if (n > 0) {
227 // If we have any visible characters, add one to the display so
228 // that the position is immediately after the data.
229 return n + 1;
230 }
231 return n;
232 }
233
234 /**
235 * Get the raw string that matches this line.
236 *
237 * @return the string
238 */
239 public String getRawString() {
240 return rawText.toString();
241 }
242
243 /**
244 * Get the raw length of this line.
245 *
246 * @return the length of this line in characters, which may be different
247 * from the number of cells needed to display it
248 */
249 public int length() {
250 return rawText.length();
251 }
252
253 /**
254 * Scan rawText and make words out of it. Note package private access.
255 */
256 void scanLine() {
257 words.clear();
258 Word word = new Word(this.defaultColor, this.highlighter);
259 words.add(word);
260 for (int i = 0; i < rawText.length();) {
261 int ch = rawText.codePointAt(i);
262 i += Character.charCount(ch);
263 Word newWord = word.addChar(ch);
264 if (newWord != word) {
265 words.add(newWord);
266 word = newWord;
267 }
268 }
269 for (Word w: words) {
270 w.applyHighlight();
271 }
272 }
273
274 /**
275 * Decrement the cursor by one. If at the first column, do nothing.
276 *
277 * @return true if the cursor position changed
278 */
279 public boolean left() {
280 if (position == 0) {
281 return false;
282 }
283 screenPosition -= StringUtils.width(rawText.codePointBefore(position));
284 position -= Character.charCount(rawText.codePointBefore(position));
285 return true;
286 }
287
288 /**
289 * Increment the cursor by one. If at the last column, do nothing.
290 *
291 * @return true if the cursor position changed
292 */
293 public boolean right() {
294 if (getDisplayLength() == 0) {
295 return false;
296 }
297 if (screenPosition == getDisplayLength() - 1) {
298 return false;
299 }
300 if (position < rawText.length()) {
301 screenPosition += StringUtils.width(rawText.codePointAt(position));
302 position += Character.charCount(rawText.codePointAt(position));
303 }
304 assert (position <= rawText.length());
305 return true;
306 }
307
308 /**
309 * Go to the first column of this line.
310 *
311 * @return true if the cursor position changed
312 */
313 public boolean home() {
314 if (position > 0) {
315 position = 0;
316 screenPosition = 0;
317 return true;
318 }
319 return false;
320 }
321
322 /**
323 * Go to the last column of this line.
324 *
325 * @return true if the cursor position changed
326 */
327 public boolean end() {
328 if (screenPosition != getDisplayLength() - 1) {
329 position = rawText.length();
330 screenPosition = StringUtils.width(rawText.toString());
331 return true;
332 }
333 return false;
334 }
335
336 /**
337 * Delete the character under the cursor.
338 */
339 public void del() {
340 assert (words.size() > 0);
341
342 if (screenPosition < getDisplayLength()) {
343 int n = Character.charCount(rawText.codePointAt(position));
344 for (int i = 0; i < n; i++) {
345 rawText.deleteCharAt(position);
346 }
347 }
348
349 // Re-scan the line to determine the new word boundaries.
350 scanLine();
351 }
352
353 /**
354 * Delete the character immediately preceeding the cursor.
355 *
356 * @param tabSize the tab stop size
357 * @param backspaceUnindents If true, backspace at an indent level goes
358 * back a full indent level. If false, backspace always goes back one
359 * column.
360 */
361 public void backspace(final int tabSize, final boolean backspaceUnindents) {
362 if ((backspaceUnindents == true)
363 && (tabSize > 0)
364 && (screenPosition > 0)
365 && (rawText.charAt(position - 1) == ' ')
366 && ((screenPosition % tabSize) == 0)
367 ) {
368 boolean doBackTab = true;
369 for (int i = 0; i < position; i++) {
370 if (rawText.charAt(i) != ' ') {
371 doBackTab = false;
372 break;
373 }
374 }
375 if (doBackTab) {
376 backTab(tabSize);
377 return;
378 }
379 }
380
381 if (left()) {
382 del();
383 }
384 }
385
386 /**
387 * Insert a character at the cursor.
388 *
389 * @param ch the character to insert
390 */
391 public void addChar(final int ch) {
392 if (screenPosition < getDisplayLength() - 1) {
393 rawText.insert(position, Character.toChars(ch));
394 } else {
395 rawText.append(Character.toChars(ch));
396 }
397 position += Character.charCount(ch);
398 screenPosition += StringUtils.width(ch);
399 scanLine();
400 }
401
402 /**
403 * Replace a character at the cursor.
404 *
405 * @param ch the character to replace
406 */
407 public void replaceChar(final int ch) {
408 if (screenPosition < getDisplayLength() - 1) {
409 // Replace character
410 String oldText = rawText.toString();
411 rawText = new StringBuilder(oldText.substring(0, position));
412 rawText.append(Character.toChars(ch));
413 rawText.append(oldText.substring(position + 1));
414 screenPosition += StringUtils.width(rawText.codePointAt(position));
415 position += Character.charCount(ch);
416 } else {
417 rawText.append(Character.toChars(ch));
418 position += Character.charCount(ch);
419 screenPosition += StringUtils.width(ch);
420 }
421 scanLine();
422 }
423
424 /**
425 * Determine string position from screen position.
426 *
427 * @param screenPosition the position on screen
428 * @return the equivalent position in text
429 */
430 private int screenToTextPosition(final int screenPosition) {
431 if (screenPosition == 0) {
432 return 0;
433 }
434
435 int n = 0;
436 for (int i = 0; i < rawText.length(); i++) {
437 n += StringUtils.width(rawText.codePointAt(i));
438 if (n >= screenPosition) {
439 return i + 1;
440 }
441 }
442 // screenPosition exceeds the available text length.
443 throw new IndexOutOfBoundsException("screenPosition " + screenPosition +
444 " exceeds available text length " + rawText.length());
445 }
446
447 /**
448 * Trim trailing whitespace from line, repositioning cursor if needed.
449 */
450 public void trimRight() {
451 if (rawText.length() == 0) {
452 return;
453 }
454 if (!Character.isWhitespace(rawText.charAt(rawText.length() - 1))) {
455 return;
456 }
457 while ((rawText.length() > 0)
458 && Character.isWhitespace(rawText.charAt(rawText.length() - 1))
459 ) {
460 rawText.deleteCharAt(rawText.length() - 1);
461 }
462 if (position >= rawText.length()) {
463 end();
464 }
465 scanLine();
466 }
467
468 /**
469 * Handle the tab character.
470 *
471 * @param tabSize the tab stop size
472 */
473 public void tab(final int tabSize) {
474 if (tabSize > 0) {
475 do {
476 addChar(' ');
477 } while ((screenPosition % tabSize) != 0);
478 }
479 }
480
481 /**
482 * Handle the backtab (shift-tab) character.
483 *
484 * @param tabSize the tab stop size
485 */
486 public void backTab(final int tabSize) {
487 if ((tabSize > 0) && (screenPosition > 0)
488 && (rawText.charAt(position - 1) == ' ')
489 ) {
490 do {
491 backspace(tabSize, false);
492 } while (((screenPosition % tabSize) != 0)
493 && (screenPosition > 0)
494 && (rawText.charAt(position - 1) == ' '));
495 }
496 }
497
498 }