From 21460f442b7b21d02c6f80290acd901d9c5a8a0b Mon Sep 17 00:00:00 2001 From: Kevin Lamonte Date: Fri, 29 Nov 2019 12:54:52 -0600 Subject: [PATCH] retrofit --- README.md | 2 +- src/jexer/TApplication.java | 30 +++- src/jexer/TEditorWidget.java | 211 +++++++++++++++++++++++--- src/jexer/TEditorWindow.java | 77 +++++++--- src/jexer/backend/SwingComponent.java | 13 +- src/jexer/backend/SwingTerminal.java | 25 +-- src/jexer/menu/TMenu.java | 18 ++- src/jexer/menu/TMenu.properties | 2 + src/jexer/teditor/Document.java | 167 +++++++++++++++++++- src/jexer/teditor/Highlighter.java | 62 +++++++- src/jexer/teditor/Line.java | 153 +++++++++++++++++-- src/jexer/teditor/Word.java | 2 - 12 files changed, 673 insertions(+), 89 deletions(-) diff --git a/README.md b/README.md index adc81a9..4e24d8c 100644 --- a/README.md +++ b/README.md @@ -181,7 +181,7 @@ The table below lists terminals tested against Jexer's Xterm backend: 5 - Sixel images can crash terminal. -6 - Version 0.4.2382.0, on Windows 10.0.18362.30. Tested against +6 - Version 0.7.3291.0, on Windows 10.0.18362.30. Tested against WSL-1 Debian instance. diff --git a/src/jexer/TApplication.java b/src/jexer/TApplication.java index e2c3cd6..28e3509 100644 --- a/src/jexer/TApplication.java +++ b/src/jexer/TApplication.java @@ -808,13 +808,26 @@ public class TApplication implements Runnable { } // Load the help system - try { - ClassLoader loader = Thread.currentThread().getContextClassLoader(); - helpFile = new HelpFile(); - helpFile.load(loader.getResourceAsStream("help.xml")); - } catch (Exception e) { - new TExceptionDialog(this, e); - } + invokeLater(new Runnable() { + /* + * This isn't the best solution. But basically if a TApplication + * subclass constructor throws and needs to use TExceptionDialog, + * it may end up at the bottom of the window stack with a bunch + * of modal windows on top of it if said constructors spawn their + * windows also via invokeLater(). But if they don't do that, + * and instead just conventionally construct their windows, then + * this exception dialog will end up on top where it should be. + */ + public void run() { + try { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + helpFile = new HelpFile(); + helpFile.load(loader.getResourceAsStream("help.xml")); + } catch (Exception e) { + new TExceptionDialog(TApplication.this, e); + } + } + }); } // ------------------------------------------------------------------------ @@ -3417,6 +3430,9 @@ public class TApplication implements Runnable { */ public final TMenu addEditMenu() { TMenu editMenu = addMenu(i18n.getString("editMenuTitle")); + editMenu.addDefaultItem(TMenu.MID_UNDO, false); + editMenu.addDefaultItem(TMenu.MID_REDO, false); + editMenu.addSeparator(); editMenu.addDefaultItem(TMenu.MID_CUT, false); editMenu.addDefaultItem(TMenu.MID_COPY, false); editMenu.addDefaultItem(TMenu.MID_PASTE, false); diff --git a/src/jexer/TEditorWidget.java b/src/jexer/TEditorWidget.java index 6ff39e6..bea25ed 100644 --- a/src/jexer/TEditorWidget.java +++ b/src/jexer/TEditorWidget.java @@ -29,6 +29,8 @@ package jexer; import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import jexer.bits.CellAttributes; import jexer.bits.StringUtils; @@ -64,10 +66,10 @@ public class TEditorWidget extends TWidget implements EditMenuUser { /** * The document being edited. */ - private Document document; + protected Document document; /** - * The default color for the TEditor class. + * The default color for the editable text. */ private CellAttributes defaultColor = null; @@ -106,6 +108,42 @@ public class TEditorWidget extends TWidget implements EditMenuUser { */ private int selectionLine1; + /** + * The list of undo/redo states. + */ + private List undoList = new ArrayList(); + + /** + * The position in undoList for undo/redo. + */ + private int undoListI = 0; + + /** + * The maximum size of the undo list. + */ + private int undoLevel = 50; + + /** + * The saved state for an undo/redo operation. + */ + private class SavedState { + /** + * The Document state. + */ + public Document document; + + /** + * The topmost line number in the visible area. 0-based. + */ + public int topLine = 0; + + /** + * The leftmost column number in the visible area. 0-based. + */ + public int leftColumn = 0; + + } + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -291,13 +329,23 @@ public class TEditorWidget extends TWidget implements EditMenuUser { @Override public void onKeypress(final TKeypressEvent keypress) { if (keypress.getKey().isShift()) { - // Selection. - if (!inSelection) { - inSelection = true; - selectionColumn0 = document.getCursor(); - selectionLine0 = document.getLineNumber(); - selectionColumn1 = selectionColumn0; - selectionLine1 = selectionLine0; + if (keypress.equals(kbShiftLeft) + || keypress.equals(kbShiftRight) + || keypress.equals(kbShiftUp) + || keypress.equals(kbShiftDown) + || keypress.equals(kbShiftPgDn) + || keypress.equals(kbShiftPgUp) + || keypress.equals(kbShiftHome) + || keypress.equals(kbShiftEnd) + ) { + // Shifted navigation keys enable selection + if (!inSelection) { + inSelection = true; + selectionColumn0 = document.getCursor(); + selectionLine0 = document.getLineNumber(); + selectionColumn1 = selectionColumn0; + selectionLine1 = selectionLine0; + } } } else { if (keypress.equals(kbLeft) @@ -396,32 +444,40 @@ public class TEditorWidget extends TWidget implements EditMenuUser { document.end(); alignTopLine(false); } else if (keypress.equals(kbIns)) { - document.setOverwrite(!document.getOverwrite()); + document.setOverwrite(!document.isOverwrite()); } else if (keypress.equals(kbDel)) { if (inSelection) { deleteSelection(); + alignCursor(); } else { + saveUndo(); document.del(); + alignCursor(); } - alignCursor(); } else if (keypress.equals(kbBackspace) || keypress.equals(kbBackspaceDel) ) { if (inSelection) { deleteSelection(); + alignTopLine(false); } else { + saveUndo(); document.backspace(); + alignTopLine(false); } - alignTopLine(false); } else if (keypress.equals(kbTab)) { deleteSelection(); - // Add spaces until we hit modulo 8. - for (int i = document.getCursor(); (i + 1) % 8 != 0; i++) { - document.addChar(' '); - } + saveUndo(); + document.tab(); + alignCursor(); + } else if (keypress.equals(kbShiftTab)) { + deleteSelection(); + saveUndo(); + document.backTab(); alignCursor(); } else if (keypress.equals(kbEnter)) { deleteSelection(); + saveUndo(); document.enter(); alignTopLine(true); } else if (!keypress.getKey().isFnKey() @@ -430,6 +486,7 @@ public class TEditorWidget extends TWidget implements EditMenuUser { ) { // Plain old keystroke, process it deleteSelection(); + saveUndo(); document.addChar(keypress.getKey().getChar()); alignCursor(); } else { @@ -499,8 +556,21 @@ public class TEditorWidget extends TWidget implements EditMenuUser { if (text != null) { for (int i = 0; i < text.length(); ) { int ch = text.codePointAt(i); - onKeypress(new TKeypressEvent(false, 0, ch, false, false, - false)); + switch (ch) { + case '\n': + onKeypress(new TKeypressEvent(kbEnter)); + break; + case '\t': + onKeypress(new TKeypressEvent(kbTab)); + break; + default: + if ((ch >= 0x20) && (ch != 0x7F)) { + onKeypress(new TKeypressEvent(false, 0, ch, + false, false, false)); + } + break; + } + i += Character.charCount(ch); } } @@ -602,6 +672,15 @@ public class TEditorWidget extends TWidget implements EditMenuUser { // TEditorWidget ---------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Set the undo level. + * + * @param undoLevel the maximum number of undo operations + */ + public void setUndoLevel(final int undoLevel) { + this.undoLevel = undoLevel; + } + /** * Align visible area with document current line. * @@ -836,6 +915,15 @@ public class TEditorWidget extends TWidget implements EditMenuUser { document.setNotDirty(); } + /** + * Get the overwrite value. + * + * @return true if new text will overwrite old text + */ + public boolean isOverwrite() { + return document.isOverwrite(); + } + /** * Save contents to file. * @@ -853,6 +941,9 @@ public class TEditorWidget extends TWidget implements EditMenuUser { if (!inSelection) { return; } + + saveUndo(); + inSelection = false; int startCol = selectionColumn0; @@ -1191,8 +1282,20 @@ public class TEditorWidget extends TWidget implements EditMenuUser { for (int i = 0; i < text.length(); ) { int ch = text.codePointAt(i); - onKeypress(new TKeypressEvent(false, 0, ch, false, false, - false)); + switch (ch) { + case '\n': + onKeypress(new TKeypressEvent(kbEnter)); + break; + case '\t': + onKeypress(new TKeypressEvent(kbTab)); + break; + default: + if ((ch >= 0x20) && (ch != 0x7F)) { + onKeypress(new TKeypressEvent(false, 0, ch, + false, false, false)); + } + break; + } i += Character.charCount(ch); } } @@ -1267,4 +1370,72 @@ public class TEditorWidget extends TWidget implements EditMenuUser { return true; } + /** + * Save undo state. + */ + private void saveUndo() { + SavedState state = new SavedState(); + state.document = document.dup(); + state.topLine = topLine; + state.leftColumn = leftColumn; + if (undoLevel > 0) { + while (undoList.size() > undoLevel) { + undoList.remove(0); + } + } + undoList.add(state); + undoListI = undoList.size() - 1; + } + + /** + * Undo an edit. + */ + public void undo() { + inSelection = false; + if ((undoListI >= 0) && (undoListI < undoList.size())) { + SavedState state = undoList.get(undoListI); + document = state.document.dup(); + topLine = state.topLine; + leftColumn = state.leftColumn; + undoListI--; + setCursorY(document.getLineNumber() - topLine); + alignCursor(); + } + } + + /** + * Redo an edit. + */ + public void redo() { + inSelection = false; + if ((undoListI >= 0) && (undoListI < undoList.size())) { + SavedState state = undoList.get(undoListI); + document = state.document.dup(); + topLine = state.topLine; + leftColumn = state.leftColumn; + undoListI++; + setCursorY(document.getLineNumber() - topLine); + alignCursor(); + } + } + + /** + * Trim trailing whitespace from lines and trailing empty + * lines from the document. + */ + public void cleanWhitespace() { + document.cleanWhitespace(); + setCursorY(document.getLineNumber() - topLine); + alignCursor(); + } + + /** + * Set keyword highlighting. + * + * @param enabled if true, enable keyword highlighting + */ + public void setHighlighting(final boolean enabled) { + document.setHighlighting(enabled); + } + } diff --git a/src/jexer/TEditorWindow.java b/src/jexer/TEditorWindow.java index d78185c..a28376b 100644 --- a/src/jexer/TEditorWindow.java +++ b/src/jexer/TEditorWindow.java @@ -44,8 +44,10 @@ import jexer.bits.CellAttributes; import jexer.bits.GraphicsChars; import jexer.event.TCommandEvent; import jexer.event.TKeypressEvent; +import jexer.event.TMenuEvent; import jexer.event.TMouseEvent; import jexer.event.TResizeEvent; +import jexer.menu.TMenu; import static jexer.TCommand.*; import static jexer.TKeypress.*; @@ -150,28 +152,27 @@ public class TEditorWindow extends TScrollableWindow { } // ------------------------------------------------------------------------ - // TWindow ---------------------------------------------------------------- + // Event handlers --------------------------------------------------------- // ------------------------------------------------------------------------ /** - * Draw the window. + * Called by application.switchWindow() when this window gets the + * focus, and also by application.addWindow(). */ - @Override - public void draw() { - // Draw as normal. - super.draw(); - - // Add the row:col on the bottom row - CellAttributes borderColor = getBorder(); - String location = String.format(" %d:%d ", - editField.getEditingRowNumber(), - editField.getEditingColumnNumber()); - int colon = location.indexOf(':'); - putStringXY(10 - colon, getHeight() - 1, location, borderColor); + public void onFocus() { + super.onFocus(); + getApplication().enableMenuItem(TMenu.MID_UNDO); + getApplication().enableMenuItem(TMenu.MID_REDO); + } - if (editField.isDirty()) { - putCharXY(2, getHeight() - 1, GraphicsChars.OCTOSTAR, borderColor); - } + /** + * Called by application.switchWindow() when another window gets the + * focus. + */ + public void onUnfocus() { + super.onUnfocus(); + getApplication().disableMenuItem(TMenu.MID_UNDO); + getApplication().disableMenuItem(TMenu.MID_REDO); } /** @@ -351,6 +352,48 @@ public class TEditorWindow extends TScrollableWindow { super.onCommand(command); } + /** + * Handle posted menu events. + * + * @param menu menu event + */ + @Override + public void onMenu(final TMenuEvent menu) { + switch (menu.getId()) { + case TMenu.MID_UNDO: + editField.undo(); + break; + case TMenu.MID_REDO: + editField.redo(); + break; + } + } + + // ------------------------------------------------------------------------ + // TWindow ---------------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Draw the window. + */ + @Override + public void draw() { + // Draw as normal. + super.draw(); + + // Add the row:col on the bottom row + CellAttributes borderColor = getBorder(); + String location = String.format(" %d:%d ", + editField.getEditingRowNumber(), + editField.getEditingColumnNumber()); + int colon = location.indexOf(':'); + putStringXY(10 - colon, getHeight() - 1, location, borderColor); + + if (editField.isDirty()) { + putCharXY(2, getHeight() - 1, GraphicsChars.OCTOSTAR, borderColor); + } + } + /** * Returns true if this window does not want the application-wide mouse * cursor drawn over it. diff --git a/src/jexer/backend/SwingComponent.java b/src/jexer/backend/SwingComponent.java index 3d1074c..df36333 100644 --- a/src/jexer/backend/SwingComponent.java +++ b/src/jexer/backend/SwingComponent.java @@ -83,7 +83,7 @@ class SwingComponent { * Adjustable Insets for this component. This has the effect of adding a * black border around the drawing area. */ - Insets adjustInsets = new Insets(BORDER + 5, BORDER, BORDER, BORDER); + Insets adjustInsets = null; // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- @@ -96,6 +96,16 @@ class SwingComponent { */ public SwingComponent(final JFrame frame) { this.frame = frame; + if (System.getProperty("os.name").startsWith("Linux")) { + // On my Linux dev system, a Swing frame draws its contents just + // a little off. No idea why, but I've seen it on both Debian + // and Fedora with KDE. These adjustments to the adjustments + // seem to center it OK in the frame. + adjustInsets = new Insets(BORDER + 5, BORDER, + BORDER - 3, BORDER + 2); + } else { + adjustInsets = new Insets(BORDER, BORDER, BORDER, BORDER); + } setupFrame(); } @@ -106,6 +116,7 @@ class SwingComponent { */ public SwingComponent(final JComponent component) { this.component = component; + adjustInsets = new Insets(BORDER, BORDER, BORDER, BORDER); setupComponent(); } diff --git a/src/jexer/backend/SwingTerminal.java b/src/jexer/backend/SwingTerminal.java index aa49467..0727efc 100644 --- a/src/jexer/backend/SwingTerminal.java +++ b/src/jexer/backend/SwingTerminal.java @@ -579,26 +579,12 @@ public class SwingTerminal extends LogicalScreen ) { do { do { - /* - * TODO: - * - * Under Windows and Mac (I think?), there was a problem - * with the screen not updating on the initial load. - * Adding clearPhysical() below "fixed" it, but at a - * horrible performance penalty on Linux which I am no - * longer willing to accept. - * - * Fix this in the "right" way for Windows/OSX such that - * the entire screen does not require a full redraw. - */ - // clearPhysical(); drawToSwing(); } while (swing.getBufferStrategy().contentsRestored()); swing.getBufferStrategy().show(); Toolkit.getDefaultToolkit().sync(); } while (swing.getBufferStrategy().contentsLost()); - } else { // Non-triple-buffered, call drawToSwing() once drawToSwing(); @@ -1333,7 +1319,10 @@ public class SwingTerminal extends LogicalScreen } // Enable anti-aliasing - if (gr instanceof Graphics2D) { + if ((gr instanceof Graphics2D) && (swing.getFrame() != null)) { + // Anti-aliasing on JComponent makes the hash character disappear + // for Terminus font, and also kills performance. Only enable it + // for JFrame. ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); ((Graphics2D) gr).setRenderingHint(RenderingHints.KEY_RENDERING, @@ -1344,7 +1333,6 @@ public class SwingTerminal extends LogicalScreen gr2.setColor(attrToBackgroundColor(cellColor)); gr2.fillRect(gr2x, gr2y, textWidth, textHeight); - // Handle blink and underline if (!cell.isBlink() || (cell.isBlink() && cursorBlinkVisible) @@ -1773,13 +1761,16 @@ public class SwingTerminal extends LogicalScreen } else { ch = key.getKeyChar(); } - alt = key.isAltDown(); + // Both meta and alt count as alt, thanks to Mac using alt for + // "symbols" so meta ("command") is the only other modifier left. + alt = key.isAltDown() | key.isMetaDown(); ctrl = key.isControlDown(); shift = key.isShiftDown(); /* System.err.printf("Swing Key: %s\n", key); System.err.printf(" isKey: %s\n", isKey); + System.err.printf(" meta: %s\n", key.isMetaDown()); System.err.printf(" alt: %s\n", alt); System.err.printf(" ctrl: %s\n", ctrl); System.err.printf(" shift: %s\n", shift); diff --git a/src/jexer/menu/TMenu.java b/src/jexer/menu/TMenu.java index a44de72..6a875c7 100644 --- a/src/jexer/menu/TMenu.java +++ b/src/jexer/menu/TMenu.java @@ -72,10 +72,12 @@ public class TMenu extends TWindow { public static final int MID_SHELL = 13; // Edit menu - public static final int MID_CUT = 20; - public static final int MID_COPY = 21; - public static final int MID_PASTE = 22; - public static final int MID_CLEAR = 23; + public static final int MID_UNDO = 20; + public static final int MID_REDO = 21; + public static final int MID_CUT = 22; + public static final int MID_COPY = 23; + public static final int MID_PASTE = 24; + public static final int MID_CLEAR = 25; // Search menu public static final int MID_FIND = 30; @@ -603,6 +605,14 @@ public class TMenu extends TWindow { icon = 0x1F5C1; break; + case MID_UNDO: + label = i18n.getString("menuUndo"); + key = kbCtrlZ; + break; + case MID_REDO: + label = i18n.getString("menuRedo"); + key = kbCtrlY; + break; case MID_CUT: label = i18n.getString("menuCut"); key = kbCtrlX; diff --git a/src/jexer/menu/TMenu.properties b/src/jexer/menu/TMenu.properties index 4a0f8e6..692293e 100644 --- a/src/jexer/menu/TMenu.properties +++ b/src/jexer/menu/TMenu.properties @@ -2,6 +2,8 @@ menuNew=&New menuExit=E&xit menuShell=O&S Shell menuOpen=&Open +menuUndo=&Undo +menuRedo=&Redo menuCut=Cu&t menuCopy=&Copy menuPaste=&Paste diff --git a/src/jexer/teditor/Document.java b/src/jexer/teditor/Document.java index e949503..b4a9a3b 100644 --- a/src/jexer/teditor/Document.java +++ b/src/jexer/teditor/Document.java @@ -76,6 +76,23 @@ public class Document { */ private Highlighter highlighter = new Highlighter(); + /** + * The tab stop size. + */ + private int tabSize = 8; + + /** + * If true, backspace at an indent level goes back a full indent level. + * If false, backspace always goes back one column. + */ + private boolean backspaceUnindents = false; + + /** + * If true, save files with tab characters. If false, convert tabs to + * spaces when saving files. + */ + private boolean saveWithTabs = false; + // ------------------------------------------------------------------------ // Constructors ----------------------------------------------------------- // ------------------------------------------------------------------------ @@ -99,16 +116,41 @@ public class Document { } } + /** + * Private constructor used by dup(). + */ + private Document() { + // NOP + } + // ------------------------------------------------------------------------ // Document --------------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Create a duplicate instance. + * + * @return duplicate intance + */ + public Document dup() { + Document other = new Document(); + for (Line line: lines) { + other.lines.add(line.dup()); + } + other.lineNumber = lineNumber; + other.overwrite = overwrite; + other.dirty = dirty; + other.defaultColor = defaultColor; + other.highlighter.setTo(highlighter); + return other; + } + /** * Get the overwrite flag. * * @return true if addChar() overwrites data, false if it inserts */ - public boolean getOverwrite() { + public boolean isOverwrite() { return overwrite; } @@ -141,7 +183,11 @@ public class Document { "UTF-8"); for (Line line: lines) { - output.write(line.getRawString()); + if (saveWithTabs) { + output.write(convertSpacesToTabs(line.getRawString())); + } else { + output.write(line.getRawString()); + } output.write("\n"); } @@ -551,7 +597,7 @@ public class Document { dirty = true; int cursor = lines.get(lineNumber).getCursor(); if (cursor > 0) { - lines.get(lineNumber).backspace(); + lines.get(lineNumber).backspace(tabSize, backspaceUnindents); } else if (lineNumber > 0) { // Join two lines lineNumber--; @@ -603,6 +649,62 @@ public class Document { } } + /** + * Get the tab stop size. + * + * @return the tab stop size + */ + public int getTabSize() { + return tabSize; + } + + /** + * Set the tab stop size. + * + * @param tabSize the new tab stop size + */ + public void setTabSize(final int tabSize) { + this.tabSize = tabSize; + } + + /** + * Set the backspace unindent option. + * + * @param backspaceUnindents If true, backspace at an indent level goes + * back a full indent level. If false, backspace always goes back one + * column. + */ + public void setBackspaceUnindents(final boolean backspaceUnindents) { + this.backspaceUnindents = backspaceUnindents; + } + + /** + * Set the save with tabs option. + * + * @param saveWithTabs If true, save files with tab characters. If + * false, convert tabs to spaces when saving files. + */ + public void setSaveWithTabs(final boolean saveWithTabs) { + this.saveWithTabs = saveWithTabs; + } + + /** + * Handle the tab character. + */ + public void tab() { + if (overwrite) { + del(); + } + lines.get(lineNumber).tab(tabSize); + } + + /** + * Handle the backtab (shift-tab) character. + */ + public void backTab() { + lines.get(lineNumber).backTab(tabSize); + } + /** * Get a (shallow) copy of the list of lines. * @@ -659,4 +761,63 @@ public class Document { return sb.toString(); } + /** + * Trim trailing whitespace from lines and trailing empty + * lines from the document. + */ + public void cleanWhitespace() { + for (Line line: getLines()) { + line.trimRight(); + } + if (lines.size() == 0) { + return; + } + while (lines.get(lines.size() - 1).length() == 0) { + lines.remove(lines.size() - 1); + } + if (lineNumber > lines.size() - 1) { + lineNumber = lines.size() - 1; + } + } + + /** + * Set keyword highlighting. + * + * @param enabled if true, enable keyword highlighting + */ + public void setHighlighting(final boolean enabled) { + highlighter.setEnabled(enabled); + for (Line line: getLines()) { + line.scanLine(); + } + } + + /** + * Convert a string with leading spaces to a mix of tabs and spaces. + * + * @param string the string to convert + */ + private String convertSpacesToTabs(final String string) { + if (string.length() == 0) { + return string; + } + + int start = 0; + while (string.charAt(start) == ' ') { + start++; + } + int tabCount = start / 8; + if (tabCount == 0) { + return string; + } + + StringBuilder sb = new StringBuilder(string.length()); + + for (int i = 0; i < tabCount; i++) { + sb.append('\t'); + } + sb.append(string.substring(tabCount * 8)); + return sb.toString(); + } + } diff --git a/src/jexer/teditor/Highlighter.java b/src/jexer/teditor/Highlighter.java index 44a2ed0..23ee900 100644 --- a/src/jexer/teditor/Highlighter.java +++ b/src/jexer/teditor/Highlighter.java @@ -56,13 +56,36 @@ public class Highlighter { * Public constructor sets the theme to the default. */ public Highlighter() { - colors = new TreeMap(); + // NOP } // ------------------------------------------------------------------------ // Highlighter ------------------------------------------------------------ // ------------------------------------------------------------------------ + /** + * Set keyword highlighting. + * + * @param enabled if true, enable keyword highlighting + */ + public void setEnabled(final boolean enabled) { + if (enabled) { + setJavaColors(); + } else { + colors = null; + } + } + + /** + * Set my field values to that's field. + * + * @param rhs an instance of Highlighter + */ + public void setTo(final Highlighter rhs) { + colors = new TreeMap(); + colors.putAll(rhs.colors); + } + /** * See if this is a character that should split a word. * @@ -87,6 +110,9 @@ public class Highlighter { * @return color associated with name, e.g. bold yellow on blue */ public CellAttributes getColor(final String name) { + if (colors == null) { + return null; + } CellAttributes attr = colors.get(name); return attr; } @@ -95,19 +121,41 @@ public class Highlighter { * Sets to defaults that resemble the Borland IDE colors. */ public void setJavaColors() { + colors = new TreeMap(); + CellAttributes color; - String [] keywords = { + String [] types = { "boolean", "byte", "short", "int", "long", "char", "float", - "double", "void", "new", - "static", "final", "volatile", "synchronized", "abstract", - "public", "private", "protected", - "class", "interface", "extends", "implements", + "double", "void", + }; + color = new CellAttributes(); + color.setForeColor(Color.GREEN); + color.setBackColor(Color.BLUE); + color.setBold(true); + for (String str: types) { + colors.put(str, color); + } + + String [] modifiers = { + "abstract", "final", "native", "private", "protected", "public", + "static", "strictfp", "synchronized", "transient", "volatile", + }; + color = new CellAttributes(); + color.setForeColor(Color.WHITE); + color.setBackColor(Color.BLUE); + color.setBold(true); + for (String str: modifiers) { + colors.put(str, color); + } + + String [] keywords = { + "new", "class", "interface", "extends", "implements", "if", "else", "do", "while", "for", "break", "continue", "switch", "case", "default", }; color = new CellAttributes(); - color.setForeColor(Color.WHITE); + color.setForeColor(Color.YELLOW); color.setBackColor(Color.BLUE); color.setBold(true); for (String str: keywords) { diff --git a/src/jexer/teditor/Line.java b/src/jexer/teditor/Line.java index 7cd5feb..b5c980a 100644 --- a/src/jexer/teditor/Line.java +++ b/src/jexer/teditor/Line.java @@ -32,6 +32,7 @@ import java.util.ArrayList; import java.util.List; import jexer.bits.CellAttributes; +import jexer.bits.GraphicsChars; import jexer.bits.StringUtils; /** @@ -92,7 +93,31 @@ public class Line { this.defaultColor = defaultColor; this.highlighter = highlighter; - this.rawText = new StringBuilder(str); + + this.rawText = new StringBuilder(); + int col = 0; + for (int i = 0; i < str.length(); i++) { + char ch = str.charAt(i); + if (ch == '\t') { + // Expand tabs + int j = col % 8; + do { + rawText.append(' '); + j++; + col++; + } while ((j % 8) != 0); + continue; + } + if ((ch <= 0x20) || (ch == 0x7F)) { + // Replace all other C0 bytes with CP437 glyphs. + rawText.append(GraphicsChars.CP437[(int) ch]); + col++; + continue; + } + + rawText.append(ch); + col++; + } scanLine(); } @@ -107,10 +132,33 @@ public class Line { this(str, defaultColor, null); } + /** + * Private constructor used by dup(). + */ + private Line() { + // NOP + } + // ------------------------------------------------------------------------ // Line ------------------------------------------------------------------- // ------------------------------------------------------------------------ + /** + * Create a duplicate instance. + * + * @return duplicate intance + */ + public Line dup() { + Line other = new Line(); + other.defaultColor = defaultColor; + other.highlighter = highlighter; + other.position = position; + other.screenPosition = screenPosition; + other.rawText = new StringBuilder(rawText); + other.scanLine(); + return other; + } + /** * Get a (shallow) copy of the words in this line. * @@ -193,9 +241,19 @@ public class Line { } /** - * Scan rawText and make words out of it. + * Get the raw length of this line. + * + * @return the length of this line in characters, which may be different + * from the number of cells needed to display it + */ + public int length() { + return rawText.length(); + } + + /** + * Scan rawText and make words out of it. Note package private access. */ - private void scanLine() { + void scanLine() { words.clear(); Word word = new Word(this.defaultColor, this.highlighter); words.add(word); @@ -236,7 +294,7 @@ public class Line { if (getDisplayLength() == 0) { return false; } - if (position == getDisplayLength() - 1) { + if (screenPosition == getDisplayLength() - 1) { return false; } if (position < rawText.length()) { @@ -267,7 +325,7 @@ public class Line { * @return true if the cursor position changed */ public boolean end() { - if (position != getDisplayLength() - 1) { + if (screenPosition != getDisplayLength() - 1) { position = rawText.length(); screenPosition = StringUtils.width(rawText.toString()); return true; @@ -281,7 +339,7 @@ public class Line { public void del() { assert (words.size() > 0); - if (position < getDisplayLength()) { + if (screenPosition < getDisplayLength()) { int n = Character.charCount(rawText.codePointAt(position)); for (int i = 0; i < n; i++) { rawText.deleteCharAt(position); @@ -294,8 +352,32 @@ public class Line { /** * Delete the character immediately preceeding the cursor. + * + * @param tabSize the tab stop size + * @param backspaceUnindents If true, backspace at an indent level goes + * back a full indent level. If false, backspace always goes back one + * column. */ - public void backspace() { + public void backspace(final int tabSize, final boolean backspaceUnindents) { + if ((backspaceUnindents == true) + && (tabSize > 0) + && (screenPosition > 0) + && (rawText.charAt(position - 1) == ' ') + && ((screenPosition % tabSize) == 0) + ) { + boolean doBackTab = true; + for (int i = 0; i < position; i++) { + if (rawText.charAt(i) != ' ') { + doBackTab = false; + break; + } + } + if (doBackTab) { + backTab(tabSize); + return; + } + } + if (left()) { del(); } @@ -307,7 +389,7 @@ public class Line { * @param ch the character to insert */ public void addChar(final int ch) { - if (position < getDisplayLength() - 1) { + if (screenPosition < getDisplayLength() - 1) { rawText.insert(position, Character.toChars(ch)); } else { rawText.append(Character.toChars(ch)); @@ -323,7 +405,7 @@ public class Line { * @param ch the character to replace */ public void replaceChar(final int ch) { - if (position < getDisplayLength() - 1) { + if (screenPosition < getDisplayLength() - 1) { // Replace character String oldText = rawText.toString(); rawText = new StringBuilder(oldText.substring(0, position)); @@ -345,7 +427,7 @@ public class Line { * @param screenPosition the position on screen * @return the equivalent position in text */ - protected int screenToTextPosition(final int screenPosition) { + private int screenToTextPosition(final int screenPosition) { if (screenPosition == 0) { return 0; } @@ -362,4 +444,55 @@ public class Line { " exceeds available text length " + rawText.length()); } + /** + * Trim trailing whitespace from line, repositioning cursor if needed. + */ + public void trimRight() { + if (rawText.length() == 0) { + return; + } + if (!Character.isWhitespace(rawText.charAt(rawText.length() - 1))) { + return; + } + while ((rawText.length() > 0) + && Character.isWhitespace(rawText.charAt(rawText.length() - 1)) + ) { + rawText.deleteCharAt(rawText.length() - 1); + } + if (position >= rawText.length()) { + end(); + } + scanLine(); + } + + /** + * Handle the tab character. + * + * @param tabSize the tab stop size + */ + public void tab(final int tabSize) { + if (tabSize > 0) { + do { + addChar(' '); + } while ((screenPosition % tabSize) != 0); + } + } + + /** + * Handle the backtab (shift-tab) character. + * + * @param tabSize the tab stop size + */ + public void backTab(final int tabSize) { + if ((tabSize > 0) && (screenPosition > 0) + && (rawText.charAt(position - 1) == ' ') + ) { + do { + backspace(tabSize, false); + } while (((screenPosition % tabSize) != 0) + && (screenPosition > 0) + && (rawText.charAt(position - 1) == ' ')); + } + } + } diff --git a/src/jexer/teditor/Word.java b/src/jexer/teditor/Word.java index 9a25d81..483f9c3 100644 --- a/src/jexer/teditor/Word.java +++ b/src/jexer/teditor/Word.java @@ -135,8 +135,6 @@ public class Word { * @return the number of cells needed to display this word */ public int getDisplayLength() { - // TODO: figure out how to handle the tab character. Do we have a - // global tab stops list and current word position? return StringUtils.width(text.toString()); } -- 2.27.0