X-Git-Url: http://git.nikiroo.be/?a=blobdiff_plain;f=src%2Fjexer%2Fteditor%2FDocument.java;h=b4a9a3bfb6b3ab476d29b45b3a01b4fbdca77816;hb=505be508ae7d3fb48122be548b310a238cfb91eb;hp=42082e55ad21a670b31a91e8ba6e9b7e0950344e;hpb=71a389c9810382e014682dde52e94d3f34e385fa;p=fanfix.git diff --git a/src/jexer/teditor/Document.java b/src/jexer/teditor/Document.java index 42082e5..b4a9a3b 100644 --- a/src/jexer/teditor/Document.java +++ b/src/jexer/teditor/Document.java @@ -3,7 +3,7 @@ * * The MIT License (MIT) * - * Copyright (C) 2017 Kevin Lamonte + * Copyright (C) 2019 Kevin Lamonte * * Permission is hereby granted, free of charge, to any person obtaining a * copy of this software and associated documentation files (the "Software"), @@ -41,6 +41,10 @@ import jexer.bits.CellAttributes; */ public class Document { + // ------------------------------------------------------------------------ + // Variables -------------------------------------------------------------- + // ------------------------------------------------------------------------ + /** * The list of lines. */ @@ -72,12 +76,81 @@ 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 ----------------------------------------------------------- + // ------------------------------------------------------------------------ + + /** + * Construct a new Document from an existing text string. + * + * @param str the text string + * @param defaultColor the color for unhighlighted text + */ + public Document(final String str, final CellAttributes defaultColor) { + this.defaultColor = defaultColor; + + // Set colors to resemble the Borland IDE colors, but for Java + // language keywords. + highlighter.setJavaColors(); + + String [] rawLines = str.split("\n"); + for (int i = 0; i < rawLines.length; i++) { + lines.add(new Line(rawLines[i], this.defaultColor, highlighter)); + } + } + + /** + * 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; } @@ -90,6 +163,13 @@ public class Document { return dirty; } + /** + * Unset the dirty flag. + */ + public void setNotDirty() { + dirty = false; + } + /** * Save contents to file. * @@ -103,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"); } @@ -180,29 +264,24 @@ public class Document { } /** - * Set the current cursor position of the editing line. 0-based. + * Get the character at the current cursor position in the text. * - * @param cursor the new cursor position + * @return the character, or -1 if the cursor is at the end of the line */ - public void setCursor(final int cursor) { - lines.get(lineNumber).setCursor(cursor); + public int getChar() { + return lines.get(lineNumber).getChar(); } /** - * Construct a new Document from an existing text string. + * Set the current cursor position of the editing line. 0-based. * - * @param str the text string - * @param defaultColor the color for unhighlighted text + * @param cursor the new cursor position */ - public Document(final String str, final CellAttributes defaultColor) { - this.defaultColor = defaultColor; - - // TODO: set different colors based on file extension - highlighter.setJavaColors(); - - String [] rawLines = str.split("\n"); - for (int i = 0; i < rawLines.length; i++) { - lines.add(new Line(rawLines[i], this.defaultColor, highlighter)); + public void setCursor(final int cursor) { + if (cursor >= lines.get(lineNumber).getDisplayLength()) { + lines.get(lineNumber).end(); + } else { + lines.get(lineNumber).setCursor(cursor); } } @@ -215,7 +294,7 @@ public class Document { if (lineNumber < lines.size() - 1) { int x = lines.get(lineNumber).getCursor(); lineNumber++; - if (x > lines.get(lineNumber).getDisplayLength()) { + if (x >= lines.get(lineNumber).getDisplayLength()) { lines.get(lineNumber).end(); } else { lines.get(lineNumber).setCursor(x); @@ -239,7 +318,7 @@ public class Document { if (lineNumber > lines.size() - 1) { lineNumber = lines.size() - 1; } - if (x > lines.get(lineNumber).getDisplayLength()) { + if (x >= lines.get(lineNumber).getDisplayLength()) { lines.get(lineNumber).end(); } else { lines.get(lineNumber).setCursor(x); @@ -258,7 +337,7 @@ public class Document { if (lineNumber > 0) { int x = lines.get(lineNumber).getCursor(); lineNumber--; - if (x > lines.get(lineNumber).getDisplayLength()) { + if (x >= lines.get(lineNumber).getDisplayLength()) { lines.get(lineNumber).end(); } else { lines.get(lineNumber).setCursor(x); @@ -282,7 +361,7 @@ public class Document { if (lineNumber < 0) { lineNumber = 0; } - if (x > lines.get(lineNumber).getDisplayLength()) { + if (x >= lines.get(lineNumber).getDisplayLength()) { lines.get(lineNumber).end(); } else { lines.get(lineNumber).setCursor(x); @@ -293,21 +372,184 @@ public class Document { } /** - * Decrement the cursor by one. If at the first column, do nothing. + * Decrement the cursor by one. If at the first column on the first + * line, do nothing. * * @return true if the cursor position changed */ public boolean left() { - return lines.get(lineNumber).left(); + if (!lines.get(lineNumber).left()) { + // We are on the leftmost column, wrap + if (up()) { + end(); + } else { + return false; + } + } + return true; } /** - * Increment the cursor by one. If at the last column, do nothing. + * Increment the cursor by one. If at the last column on the last line, + * do nothing. * * @return true if the cursor position changed */ public boolean right() { - return lines.get(lineNumber).right(); + if (!lines.get(lineNumber).right()) { + // We are on the rightmost column, wrap + if (down()) { + home(); + } else { + return false; + } + } + return true; + } + + /** + * Go back to the beginning of this word if in the middle, or the + * beginning of the previous word. + */ + public void backwardsWord() { + + // If at the beginning of a word already, push past it. + if ((getChar() != -1) + && (getRawLine().length() > 0) + && !Character.isWhitespace((char) getChar()) + ) { + left(); + } + + // int line = lineNumber; + while ((getChar() == -1) + || (getRawLine().length() == 0) + || Character.isWhitespace((char) getChar()) + ) { + if (left() == false) { + return; + } + } + + + assert (getChar() != -1); + + if (!Character.isWhitespace((char) getChar()) + && (getRawLine().length() > 0) + ) { + // Advance until at the beginning of the document or a whitespace + // is encountered. + while (!Character.isWhitespace((char) getChar())) { + int line = lineNumber; + if (left() == false) { + // End of document, bail out. + return; + } + if (lineNumber != line) { + // We wrapped a line. Here that counts as whitespace. + right(); + return; + } + } + } + + // We went one past the word, push back to the first character of + // that word. + right(); + return; + } + + /** + * Go to the beginning of the next word. + */ + public void forwardsWord() { + int line = lineNumber; + while ((getChar() == -1) + || (getRawLine().length() == 0) + ) { + if (right() == false) { + return; + } + if (lineNumber != line) { + // We wrapped a line. Here that counts as whitespace. + if (!Character.isWhitespace((char) getChar())) { + // We found a character immediately after the line. + // Done! + return; + } + // Still looking... + line = lineNumber; + } + } + assert (getChar() != -1); + + if (!Character.isWhitespace((char) getChar()) + && (getRawLine().length() > 0) + ) { + // Advance until at the end of the document or a whitespace is + // encountered. + while (!Character.isWhitespace((char) getChar())) { + line = lineNumber; + if (right() == false) { + // End of document, bail out. + return; + } + if (lineNumber != line) { + // We wrapped a line. Here that counts as whitespace. + if (!Character.isWhitespace((char) getChar()) + && (getRawLine().length() > 0) + ) { + // We found a character immediately after the line. + // Done! + return; + } + break; + } + } + } + + while ((getChar() == -1) + || (getRawLine().length() == 0) + ) { + if (right() == false) { + return; + } + if (lineNumber != line) { + // We wrapped a line. Here that counts as whitespace. + if (!Character.isWhitespace((char) getChar())) { + // We found a character immediately after the line. + // Done! + return; + } + // Still looking... + line = lineNumber; + } + } + assert (getChar() != -1); + + if (Character.isWhitespace((char) getChar())) { + // Advance until at the end of the document or a non-whitespace + // is encountered. + while (Character.isWhitespace((char) getChar())) { + if (right() == false) { + // End of document, bail out. + return; + } + } + return; + } + + // We wrapped the line to get here. + return; + } + + /** + * Get the raw string that matches this line. + * + * @return the string + */ + public String getRawLine() { + return lines.get(lineNumber).getRawString(); } /** @@ -333,7 +575,19 @@ public class Document { */ public void del() { dirty = true; - lines.get(lineNumber).del(); + int cursor = lines.get(lineNumber).getCursor(); + if (cursor < lines.get(lineNumber).getDisplayLength() - 1) { + lines.get(lineNumber).del(); + } else if (lineNumber < lines.size() - 2) { + // Join two lines + StringBuilder newLine = new StringBuilder(lines. + get(lineNumber).getRawString()); + newLine.append(lines.get(lineNumber + 1).getRawString()); + lines.set(lineNumber, new Line(newLine.toString(), + defaultColor, highlighter)); + lines.get(lineNumber).setCursor(cursor); + lines.remove(lineNumber + 1); + } } /** @@ -341,7 +595,43 @@ public class Document { */ public void backspace() { dirty = true; - lines.get(lineNumber).backspace(); + int cursor = lines.get(lineNumber).getCursor(); + if (cursor > 0) { + lines.get(lineNumber).backspace(tabSize, backspaceUnindents); + } else if (lineNumber > 0) { + // Join two lines + lineNumber--; + String firstLine = lines.get(lineNumber).getRawString(); + if (firstLine.length() > 0) { + // Backspacing combining two lines + StringBuilder newLine = new StringBuilder(firstLine); + newLine.append(lines.get(lineNumber + 1).getRawString()); + lines.set(lineNumber, new Line(newLine.toString(), + defaultColor, highlighter)); + lines.get(lineNumber).setCursor(firstLine.length()); + lines.remove(lineNumber + 1); + } else { + // Backspacing an empty line + lines.remove(lineNumber); + lines.get(lineNumber).setCursor(0); + } + } + } + + /** + * Split the current line into two, like pressing the enter key. + */ + public void enter() { + dirty = true; + int cursor = lines.get(lineNumber).getRawCursor(); + String original = lines.get(lineNumber).getRawString(); + String firstLine = original.substring(0, cursor); + String secondLine = original.substring(cursor); + lines.add(lineNumber + 1, new Line(secondLine, defaultColor, + highlighter)); + lines.set(lineNumber, new Line(firstLine, defaultColor, highlighter)); + lineNumber++; + lines.get(lineNumber).home(); } /** @@ -350,7 +640,7 @@ public class Document { * * @param ch the character to replace or insert */ - public void addChar(final char ch) { + public void addChar(final int ch) { dirty = true; if (overwrite) { lines.get(lineNumber).replaceChar(ch); @@ -359,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. * @@ -392,4 +738,86 @@ public class Document { return n; } + /** + * Get the current line length. + * + * @return the number of cells needed to display the current line + */ + public int getLineLength() { + return lines.get(lineNumber).getDisplayLength(); + } + + /** + * Get the entire contents of the document as one string. + * + * @return the document contents + */ + public String getText() { + StringBuilder sb = new StringBuilder(); + for (Line line: getLines()) { + sb.append(line.getRawString()); + sb.append("\n"); + } + 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(); + } + }