TEditor 50% complete
[nikiroo-utils.git] / src / jexer / teditor / Line.java
index e36a6c9c45826b5e3b6c56d865a9aacba6106e21..de1265982c67e3190a122a1dae0e692ba7e2a17e 100644 (file)
@@ -31,469 +31,233 @@ package jexer.teditor;
 import java.util.ArrayList;
 import java.util.List;
 
-import jexer.bits.Cell;
 import jexer.bits.CellAttributes;
 
 /**
- * A Line represents a single line of text on the screen.  Each character is
- * a Cell, so it can have color attributes in addition to the basic char.
+ * A Line represents a single line of text on the screen, as a collection of
+ * words.
  */
-public class Line implements Fragment {
+public class Line {
 
     /**
-     * The cells of the line.
+     * The list of words.
      */
-    private List<Cell> cells;
+    private ArrayList<Word> words = new ArrayList<Word>();
 
     /**
-     * The line number.
+     * The default color for the TEditor class.
      */
-    private int lineNumber;
+    private CellAttributes defaultColor = null;
 
     /**
-     * The previous Fragment in the list.
+     * The text highlighter to use.
      */
-    private Fragment prevFrag;
+    private Highlighter highlighter = null;
 
     /**
-     * The next Fragment in the list.
+     * The current cursor position on this line.
      */
-    private Fragment nextFrag;
+    private int cursor = 0;
 
     /**
-     * Construct a new Line from an existing text string.
-     */
-    public Line() {
-        this("");
-    }
-
-    /**
-     * Construct a new Line from an existing text string.
-     *
-     * @param text the code points of the line
+     * The current word that the cursor position is in.
      */
-    public Line(final String text) {
-        cells = new ArrayList<Cell>(text.length());
-        for (int i = 0; i < text.length(); i++) {
-            cells.add(new Cell(text.charAt(i)));
-        }
-    }
+    private Word currentWord;
 
     /**
-     * Reset all colors of this Line to white-on-black.
-     */
-    public void resetColors() {
-        setColors(new CellAttributes());
-    }
-
-    /**
-     * Set all colors of this Line to one color.
-     *
-     * @param color the new color to use
+     * We use getDisplayLength() a lot, so cache the value.
      */
-    public void setColors(final CellAttributes color) {
-        for (Cell cell: cells) {
-            cell.setTo(color);
-        }
-    }
+    private int displayLength = -1;
 
     /**
-     * Set the color of one cell.
+     * Get the current cursor position.
      *
-     * @param index a cell number, between 0 and getCellCount()
-     * @param color the new color to use
-     * @throws IndexOutOfBoundsException if index is negative or not less
-     * than getCellCount()
+     * @return the cursor position
      */
-    public void setColor(final int index, final CellAttributes color) {
-        cells.get(index).setTo(color);
+    public int getCursor() {
+        return cursor;
     }
 
     /**
-     * Get the raw text that will be rendered.
+     * Set the current cursor position.
      *
-     * @return the text
+     * @param cursor the new cursor position
      */
-    public String getText() {
-        char [] text = new char[cells.size()];
-        for (int i = 0; i < cells.size(); i++) {
-            text[i] = cells.get(i).getChar();
+    public void setCursor(final int cursor) {
+        if ((cursor < 0)
+            || ((cursor >= getDisplayLength())
+                && (getDisplayLength() > 0))
+        ) {
+            throw new IndexOutOfBoundsException("Max length is " +
+                getDisplayLength() + ", requested position " + cursor);
         }
-        return new String(text);
-    }
-
-    /**
-     * Get the attributes for a cell.
-     *
-     * @param index a cell number, between 0 and getCellCount()
-     * @return the attributes
-     * @throws IndexOutOfBoundsException if index is negative or not less
-     * than getCellCount()
-     */
-    public CellAttributes getColor(final int index) {
-        return cells.get(index);
+        this.cursor = cursor;
+        // TODO: set word
     }
 
     /**
-     * Get the number of graphical cells represented by this text.  Note that
-     * a Unicode grapheme cluster can take any number of pixels, but this
-     * editor is intended to be used with a fixed-width font.  So this count
-     * returns the number of fixed-width cells, NOT the number of grapheme
-     * clusters.
+     * Get a (shallow) copy of the list of words.
      *
-     * @return the number of fixed-width cells this fragment's text will
-     * render to
+     * @return the list of words
      */
-    public int getCellCount() {
-        return cells.size();
+    public List<Word> getWords() {
+        return new ArrayList<Word>(words);
     }
 
     /**
-     * Get the text to render for a specific fixed-width cell.
+     * Get the on-screen display length.
      *
-     * @param index a cell number, between 0 and getCellCount()
-     * @return the codepoints to render for this fixed-width cell
-     * @throws IndexOutOfBoundsException if index is negative or not less
-     * than getCellCount()
+     * @return the number of cells needed to display this line
      */
-    public Cell getCell(final int index) {
-        return cells.get(index);
-    }
-
-    /**
-     * Get the text to render for several fixed-width cells.
-     *
-     * @param start a cell number, between 0 and getCellCount()
-     * @param length the number of cells to return
-     * @return the codepoints to render for this fixed-width cell
-     * @throws IndexOutOfBoundsException if start or (start + length) is
-     * negative or not less than getCellCount()
-     */
-    public String getCells(final int start, final int length) {
-        char [] text = new char[length];
-        for (int i = 0; i < length; i++) {
-            text[i] = cells.get(i + start).getChar();
+    public int getDisplayLength() {
+        if (displayLength != -1) {
+            return displayLength;
         }
-        return new String(text);
-    }
-
-    /**
-     * Sets (replaces) the text to render for a specific fixed-width cell.
-     *
-     * @param index a cell number, between 0 and getCellCount()
-     * @param ch the character for this fixed-width cell
-     * @throws IndexOutOfBoundsException if index is negative or not less
-     * than getCellCount()
-     */
-    public void setCell(final int index, final char ch) {
-        cells.set(index, new Cell(ch));
-    }
-
-    /**
-     * Sets (replaces) the text to render for a specific fixed-width cell.
-     *
-     * @param index a cell number, between 0 and getCellCount()
-     * @param cell the new value for this fixed-width cell
-     * @throws IndexOutOfBoundsException if index is negative or not less
-     * than getCellCount()
-     */
-    public void setCell(final int index, final Cell cell) {
-        cells.set(index, cell);
-    }
-
-    /**
-     * Inserts a char to render for a specific fixed-width cell.
-     *
-     * @param index a cell number, between 0 and getCellCount() - 1
-     * @param ch the character for this fixed-width cell
-     * @throws IndexOutOfBoundsException if index is negative or not less
-     * than getCellCount()
-     */
-    public void insertCell(final int index, final char ch) {
-        cells.add(index, new Cell(ch));
-    }
-
-    /**
-     * Inserts a Cell to render for a specific fixed-width cell.
-     *
-     * @param index a cell number, between 0 and getCellCount() - 1
-     * @param cell the new value for this fixed-width cell
-     * @throws IndexOutOfBoundsException if index is negative or not less
-     * than getCellCount()
-     */
-    public void insertCell(final int index, final Cell cell) {
-        cells.add(index, cell);
-    }
-
-    /**
-     * Delete a specific fixed-width cell.
-     *
-     * @param index a cell number, between 0 and getCellCount() - 1
-     * @throws IndexOutOfBoundsException if index is negative or not less
-     * than getCellCount()
-     */
-    public void deleteCell(final int index) {
-        cells.remove(index);
-    }
+        int n = 0;
+        for (Word word: words) {
+            n += word.getDisplayLength();
+        }
+        displayLength = n;
 
-    /**
-     * Delete several fixed-width cells.
-     *
-     * @param start a cell number, between 0 and getCellCount() - 1
-     * @param length the number of cells to delete
-     * @throws IndexOutOfBoundsException if index is negative or not less
-     * than getCellCount()
-     */
-    public void deleteCells(final int start, final int length) {
-        for (int i = 0; i < length; i++) {
-            cells.remove(start);
+        // If we have any visible characters, add one to the display so that
+        // the cursor is immediately after the data.
+        if (displayLength > 0) {
+            displayLength++;
         }
+        return displayLength;
     }
 
     /**
-     * Appends a char to render for a specific fixed-width cell.
+     * Construct a new Line from an existing text string, and highlight
+     * certain strings.
      *
-     * @param ch the character for this fixed-width cell
+     * @param str the text string
+     * @param defaultColor the color for unhighlighted text
+     * @param highlighter the highlighter to use
      */
-    public void appendCell(final char ch) {
-        cells.add(new Cell(ch));
-    }
+    public Line(final String str, final CellAttributes defaultColor,
+        final Highlighter highlighter) {
 
-    /**
-     * Inserts a Cell to render for a specific fixed-width cell.
-     *
-     * @param cell the new value for this fixed-width cell
-     */
-    public void appendCell(final Cell cell) {
-        cells.add(cell);
-    }
+        this.defaultColor = defaultColor;
+        this.highlighter = highlighter;
 
-    /**
-     * Get the next Fragment in the list, or null if this Fragment is the
-     * last node.
-     *
-     * @return the next Fragment, or null
-     */
-    public Fragment next() {
-        return nextFrag;
+        currentWord = new Word(this.defaultColor, this.highlighter);
+        words.add(currentWord);
+        for (int i = 0; i < str.length(); i++) {
+            char ch = str.charAt(i);
+            Word newWord = currentWord.addChar(ch);
+            if (newWord != currentWord) {
+                words.add(newWord);
+                currentWord = newWord;
+            }
+        }
+        for (Word word: words) {
+            word.applyHighlight();
+        }
     }
 
     /**
-     * Get the previous Fragment in the list, or null if this Fragment is the
-     * first node.
+     * Construct a new Line from an existing text string.
      *
-     * @return the previous Fragment, or null
+     * @param str the text string
+     * @param defaultColor the color for unhighlighted text
      */
-    public Fragment prev() {
-        return prevFrag;
+    public Line(final String str, final CellAttributes defaultColor) {
+        this(str, defaultColor, null);
     }
 
     /**
-     * See if this Fragment can be joined with the next Fragment in list.
+     * Decrement the cursor by one.  If at the first column, do nothing.
      *
-     * @return true if the join was possible, false otherwise
+     * @return true if the cursor position changed
      */
-    public boolean isNextJoinable() {
-        if ((nextFrag != null) && (nextFrag instanceof Line)) {
-            return true;
+    public boolean left() {
+        if (cursor == 0) {
+            return false;
         }
-        return false;
+        // TODO: switch word
+        cursor--;
+        return true;
     }
 
     /**
-     * Join this Fragment with the next Fragment in list.
+     * Increment the cursor by one.  If at the last column, do nothing.
      *
-     * @return true if the join was successful, false otherwise
+     * @return true if the cursor position changed
      */
-    public boolean joinNext() {
-        if ((nextFrag == null) || !(nextFrag instanceof Line)) {
+    public boolean right() {
+        if (getDisplayLength() == 0) {
+            return false;
+        }
+        if (cursor == getDisplayLength() - 1) {
             return false;
         }
-        Line q = (Line) nextFrag;
-        ArrayList<Cell> newCells = new ArrayList<Cell>(this.cells.size() +
-            q.cells.size());
-        newCells.addAll(this.cells);
-        newCells.addAll(q.cells);
-        this.cells = newCells;
-        ((Line) q.nextFrag).prevFrag = this;
-        nextFrag = q.nextFrag;
+        // TODO: switch word
+        cursor++;
         return true;
     }
 
     /**
-     * See if this Fragment can be joined with the previous Fragment in list.
+     * Go to the first column of this line.
      *
-     * @return true if the join was possible, false otherwise
+     * @return true if the cursor position changed
      */
-    public boolean isPrevJoinable() {
-        if ((prevFrag != null) && (prevFrag instanceof Line)) {
+    public boolean home() {
+        if (cursor > 0) {
+            cursor = 0;
+            currentWord = words.get(0);
             return true;
         }
         return false;
     }
 
     /**
-     * Join this Fragment with the previous Fragment in list.
+     * Go to the last column of this line.
      *
-     * @return true if the join was successful, false otherwise
+     * @return true if the cursor position changed
      */
-    public boolean joinPrev() {
-        if ((prevFrag == null) || !(prevFrag instanceof Line)) {
-            return false;
+    public boolean end() {
+        if (cursor != getDisplayLength() - 1) {
+            cursor = getDisplayLength() - 1;
+            if (cursor < 0) {
+                cursor = 0;
+            }
+            currentWord = words.get(words.size() - 1);
+            return true;
         }
-        Line p = (Line) prevFrag;
-        ArrayList<Cell> newCells = new ArrayList<Cell>(this.cells.size() +
-            p.cells.size());
-        newCells.addAll(p.cells);
-        newCells.addAll(this.cells);
-        this.cells = newCells;
-        ((Line) p.prevFrag).nextFrag = this;
-        prevFrag = p.prevFrag;
-        return true;
-    }
-
-    /**
-     * Set the next Fragment in the list.  Note that this performs no sanity
-     * checking or modifications on fragment; this function can break
-     * connectivity in the list.
-     *
-     * @param fragment the next Fragment, or null
-     */
-    public void setNext(Fragment fragment) {
-        nextFrag = fragment;
-    }
-
-    /**
-     * Set the previous Fragment in the list.  Note that this performs no
-     * sanity checking or modifications on fragment; this function can break
-     * connectivity in the list.
-     *
-     * @param fragment the previous Fragment, or null
-     */
-    public void setPrev(Fragment fragment) {
-        prevFrag = fragment;
-    }
-
-    /**
-     * Split this Fragment into two.  'this' Fragment will contain length
-     * cells, 'this.next()' will contain (getCellCount() - length) cells.
-     *
-     * @param length the number of cells to leave in this Fragment
-     * @throws IndexOutOfBoundsException if length is negative, or 0, greater
-     * than (getCellCount() - 1)
-     */
-    public void split(final int length) {
-        // Create the next node
-        Line q = new Line();
-        q.nextFrag = nextFrag;
-        q.prevFrag = this;
-        ((Line) nextFrag).prevFrag = q;
-        nextFrag = q;
-
-        // Split cells
-        q.cells = new ArrayList<Cell>(cells.size() - length);
-        q.cells.addAll(cells.subList(length, cells.size()));
-        cells = cells.subList(0, length);
-    }
-
-    /**
-     * Insert a new Fragment at a position, splitting the contents of this
-     * Fragment into two around it.  'this' Fragment will contain the cells
-     * between 0 and index, 'this.next()' will be the inserted fragment, and
-     * 'this.next().next()' will contain the cells between 'index' and
-     * getCellCount() - 1.
-     *
-     * @param index the number of cells to leave in this Fragment
-     * @param fragment the Fragment to insert
-     * @throws IndexOutOfBoundsException if length is negative, or 0, greater
-     * than (getCellCount() - 1)
-     */
-    public void split(final int index, Fragment fragment) {
-        // Create the next node and insert into the list.
-        Line q = new Line();
-        q.nextFrag = nextFrag;
-        q.nextFrag.setPrev(q);
-        q.prevFrag = fragment;
-        fragment.setNext(q);
-        fragment.setPrev(this);
-        nextFrag = fragment;
-
-        // Split cells
-        q.cells = new ArrayList<Cell>(cells.size() - index);
-        q.cells.addAll(cells.subList(index, cells.size()));
-        cells = cells.subList(0, index);
-    }
-
-    /**
-     * Insert a new Fragment before this one.
-     *
-     * @param fragment the Fragment to insert
-     */
-    public void insert(Fragment fragment) {
-        fragment.setNext(this);
-        fragment.setPrev(prevFrag);
-        prevFrag.setNext(fragment);
-        prevFrag = fragment;
-    }
-
-    /**
-     * Append a new Fragment at the end of this one.
-     *
-     * @param fragment the Fragment to append
-     */
-    public void append(Fragment fragment) {
-        fragment.setNext(nextFrag);
-        fragment.setPrev(this);
-        nextFrag.setPrev(fragment);
-        nextFrag = fragment;
+        return false;
     }
 
     /**
-     * Delete this Fragment from the list, and return its next().
-     *
-     * @return this Fragment's next(), or null if it was at the end of the
-     * list
+     * Delete the character under the cursor.
      */
-    public Fragment deleteGetNext() {
-        Fragment result = nextFrag;
-        nextFrag.setPrev(prevFrag);
-        prevFrag.setNext(nextFrag);
-        prevFrag = null;
-        nextFrag = null;
-        return result;
+    public void del() {
+        // TODO
     }
 
     /**
-     * Delete this Fragment from the list, and return its prev().
-     *
-     * @return this Fragment's next(), or null if it was at the beginning of
-     * the list
+     * Delete the character immediately preceeding the cursor.
      */
-    public Fragment deleteGetPrev() {
-        Fragment result = prevFrag;
-        nextFrag.setPrev(prevFrag);
-        prevFrag.setNext(nextFrag);
-        prevFrag = null;
-        nextFrag = null;
-        return result;
+    public void backspace() {
+        // TODO
     }
 
     /**
-     * Get the anchor position.
+     * Insert a character at the cursor.
      *
-     * @return the anchor number
+     * @param ch the character to insert
      */
-    public int getAnchor() {
-        return lineNumber;
+    public void addChar(final char ch) {
+        // TODO
     }
 
     /**
-     * Set the anchor position.
+     * Replace a character at the cursor.
      *
-     * @param x the new anchor number
+     * @param ch the character to replace
      */
-    public void setAnchor(final int x) {
-        lineNumber = x;
+    public void replaceChar(final char ch) {
+        // TODO
     }
 
 }