weblib: update pg on import
[fanfix.git] / src / jexer / teditor / Line.java
CommitLineData
cd92b0ba
KL
1/*
2 * Jexer - Java Text User Interface
3 *
4 * The MIT License (MIT)
5 *
a69ed767 6 * Copyright (C) 2019 Kevin Lamonte
cd92b0ba
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
31import java.util.ArrayList;
32import java.util.List;
33
e8a11f98 34import jexer.bits.CellAttributes;
21460f44 35import jexer.bits.GraphicsChars;
2d3f60d8 36import jexer.bits.StringUtils;
e8a11f98 37
cd92b0ba 38/**
12b55d76
KL
39 * A Line represents a single line of text on the screen, as a collection of
40 * words.
cd92b0ba 41 */
12b55d76 42public class Line {
cd92b0ba 43
615a0d99
KL
44 // ------------------------------------------------------------------------
45 // Variables --------------------------------------------------------------
46 // ------------------------------------------------------------------------
47
cd92b0ba 48 /**
12b55d76 49 * The list of words.
cd92b0ba 50 */
12b55d76 51 private ArrayList<Word> words = new ArrayList<Word>();
cd92b0ba 52
e8a11f98
KL
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
cd92b0ba 63 /**
39e86397 64 * The current edition position on this line.
cd92b0ba 65 */
39e86397
KL
66 private int position = 0;
67
68 /**
69 * The current editing position screen column number.
70 */
71 private int screenPosition = 0;
cd92b0ba
KL
72
73 /**
71a389c9
KL
74 * The raw text of this line, what is passed to Word to determine
75 * highlighting behavior.
cd92b0ba 76 */
71a389c9 77 private StringBuilder rawText;
cd92b0ba 78
615a0d99
KL
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;
21460f44
KL
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 }
615a0d99
KL
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
21460f44
KL
135 /**
136 * Private constructor used by dup().
137 */
138 private Line() {
139 // NOP
140 }
141
615a0d99
KL
142 // ------------------------------------------------------------------------
143 // Line -------------------------------------------------------------------
144 // ------------------------------------------------------------------------
145
21460f44
KL
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
cd92b0ba 162 /**
71a389c9
KL
163 * Get a (shallow) copy of the words in this line.
164 *
165 * @return a copy of the word list
cd92b0ba 166 */
71a389c9
KL
167 public List<Word> getWords() {
168 return new ArrayList<Word>(words);
169 }
cd92b0ba 170
e8a11f98 171 /**
39e86397
KL
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.
e8a11f98
KL
182 *
183 * @return the cursor position
184 */
185 public int getCursor() {
39e86397 186 return screenPosition;
e8a11f98
KL
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 }
39e86397
KL
202 screenPosition = cursor;
203 position = screenToTextPosition(screenPosition);
e8a11f98
KL
204 }
205
e23989a4
KL
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
cd92b0ba 218 /**
71a389c9 219 * Get the on-screen display length.
cd92b0ba 220 *
71a389c9 221 * @return the number of cells needed to display this line
cd92b0ba 222 */
71a389c9 223 public int getDisplayLength() {
2d3f60d8 224 int n = StringUtils.width(rawText.toString());
71a389c9 225
71a389c9
KL
226 if (n > 0) {
227 // If we have any visible characters, add one to the display so
39e86397 228 // that the position is immediately after the data.
71a389c9
KL
229 return n + 1;
230 }
231 return n;
cd92b0ba
KL
232 }
233
234 /**
71a389c9 235 * Get the raw string that matches this line.
cd92b0ba 236 *
71a389c9 237 * @return the string
cd92b0ba 238 */
71a389c9
KL
239 public String getRawString() {
240 return rawText.toString();
241 }
e8a11f98 242
71a389c9 243 /**
21460f44
KL
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.
71a389c9 255 */
21460f44 256 void scanLine() {
71a389c9
KL
257 words.clear();
258 Word word = new Word(this.defaultColor, this.highlighter);
259 words.add(word);
2d3f60d8
KL
260 for (int i = 0; i < rawText.length();) {
261 int ch = rawText.codePointAt(i);
262 i += Character.charCount(ch);
71a389c9
KL
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();
e8a11f98 271 }
cd92b0ba
KL
272 }
273
cd92b0ba 274 /**
12b55d76 275 * Decrement the cursor by one. If at the first column, do nothing.
e8a11f98
KL
276 *
277 * @return true if the cursor position changed
cd92b0ba 278 */
e8a11f98 279 public boolean left() {
39e86397 280 if (position == 0) {
e8a11f98 281 return false;
cd92b0ba 282 }
39e86397
KL
283 screenPosition -= StringUtils.width(rawText.codePointBefore(position));
284 position -= Character.charCount(rawText.codePointBefore(position));
e8a11f98 285 return true;
cd92b0ba
KL
286 }
287
288 /**
12b55d76 289 * Increment the cursor by one. If at the last column, do nothing.
e8a11f98
KL
290 *
291 * @return true if the cursor position changed
cd92b0ba 292 */
e8a11f98
KL
293 public boolean right() {
294 if (getDisplayLength() == 0) {
295 return false;
cd92b0ba 296 }
21460f44 297 if (screenPosition == getDisplayLength() - 1) {
e8a11f98
KL
298 return false;
299 }
39e86397
KL
300 if (position < rawText.length()) {
301 screenPosition += StringUtils.width(rawText.codePointAt(position));
302 position += Character.charCount(rawText.codePointAt(position));
2d3f60d8 303 }
39e86397 304 assert (position <= rawText.length());
e8a11f98 305 return true;
cd92b0ba
KL
306 }
307
308 /**
12b55d76 309 * Go to the first column of this line.
e8a11f98
KL
310 *
311 * @return true if the cursor position changed
cd92b0ba 312 */
e8a11f98 313 public boolean home() {
39e86397
KL
314 if (position > 0) {
315 position = 0;
316 screenPosition = 0;
e8a11f98
KL
317 return true;
318 }
319 return false;
cd92b0ba
KL
320 }
321
322 /**
12b55d76 323 * Go to the last column of this line.
e8a11f98
KL
324 *
325 * @return true if the cursor position changed
cd92b0ba 326 */
e8a11f98 327 public boolean end() {
21460f44 328 if (screenPosition != getDisplayLength() - 1) {
39e86397
KL
329 position = rawText.length();
330 screenPosition = StringUtils.width(rawText.toString());
e8a11f98
KL
331 return true;
332 }
333 return false;
cd92b0ba
KL
334 }
335
336 /**
12b55d76 337 * Delete the character under the cursor.
cd92b0ba 338 */
12b55d76 339 public void del() {
71a389c9
KL
340 assert (words.size() > 0);
341
21460f44 342 if (screenPosition < getDisplayLength()) {
39e86397
KL
343 int n = Character.charCount(rawText.codePointAt(position));
344 for (int i = 0; i < n; i++) {
345 rawText.deleteCharAt(position);
2d3f60d8 346 }
71a389c9
KL
347 }
348
349 // Re-scan the line to determine the new word boundaries.
350 scanLine();
cd92b0ba
KL
351 }
352
353 /**
12b55d76 354 * Delete the character immediately preceeding the cursor.
21460f44
KL
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.
cd92b0ba 360 */
21460f44
KL
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
71a389c9
KL
381 if (left()) {
382 del();
383 }
cd92b0ba
KL
384 }
385
386 /**
e8a11f98 387 * Insert a character at the cursor.
cd92b0ba 388 *
e8a11f98 389 * @param ch the character to insert
cd92b0ba 390 */
2d3f60d8 391 public void addChar(final int ch) {
21460f44 392 if (screenPosition < getDisplayLength() - 1) {
39e86397 393 rawText.insert(position, Character.toChars(ch));
71a389c9 394 } else {
2d3f60d8 395 rawText.append(Character.toChars(ch));
71a389c9 396 }
39e86397
KL
397 position += Character.charCount(ch);
398 screenPosition += StringUtils.width(ch);
71a389c9 399 scanLine();
cd92b0ba
KL
400 }
401
e8a11f98
KL
402 /**
403 * Replace a character at the cursor.
404 *
405 * @param ch the character to replace
406 */
2d3f60d8 407 public void replaceChar(final int ch) {
21460f44 408 if (screenPosition < getDisplayLength() - 1) {
39e86397
KL
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);
71a389c9 416 } else {
2d3f60d8 417 rawText.append(Character.toChars(ch));
39e86397
KL
418 position += Character.charCount(ch);
419 screenPosition += StringUtils.width(ch);
71a389c9
KL
420 }
421 scanLine();
39e86397
KL
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 */
21460f44 430 private int screenToTextPosition(final int screenPosition) {
39e86397
KL
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());
e8a11f98
KL
445 }
446
21460f44
KL
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
cd92b0ba 498}