diff --git a/builtins/src/main/java/org/jline/builtins/Commands.java b/builtins/src/main/java/org/jline/builtins/Commands.java index 18fdde7ee..40ece5b5e 100644 --- a/builtins/src/main/java/org/jline/builtins/Commands.java +++ b/builtins/src/main/java/org/jline/builtins/Commands.java @@ -149,7 +149,7 @@ public static void less(Terminal terminal, InputStream in, PrintStream out, Prin tabs.add(parseInteger(s)); } } - Less less = new Less(terminal).tabs(tabs); + Less less = new Less(terminal, currentDir).tabs(tabs); less.quitAtFirstEof = opt.isSet("QUIT-AT-EOF"); less.quitAtSecondEof = opt.isSet("quit-at-eof"); less.quiet = opt.isSet("quiet"); @@ -176,7 +176,7 @@ public static void less(Terminal terminal, InputStream in, PrintStream out, Prin less.run(sources); } - private static Source doUrlSource(Path currentDir, Path file) { + protected static Source doUrlSource(Path currentDir, Path file) { Source out = null; try { out = new URLSource(currentDir.resolve(file).toUri().toURL(), file.toString()); diff --git a/builtins/src/main/java/org/jline/builtins/Completers.java b/builtins/src/main/java/org/jline/builtins/Completers.java index b4a1ca052..bbfeb302f 100644 --- a/builtins/src/main/java/org/jline/builtins/Completers.java +++ b/builtins/src/main/java/org/jline/builtins/Completers.java @@ -219,17 +219,32 @@ private boolean isTrue(Object result) { public static class DirectoriesCompleter extends FileNameCompleter { private final Supplier currentDir; + private final boolean forceSlash; public DirectoriesCompleter(File currentDir) { - this(currentDir.toPath()); + this(currentDir.toPath(), false); + } + + public DirectoriesCompleter(File currentDir, boolean forceSlash) { + this(currentDir.toPath(), forceSlash); } public DirectoriesCompleter(Path currentDir) { + this(currentDir, false); + } + + public DirectoriesCompleter(Path currentDir, boolean forceSlash) { this.currentDir = () -> currentDir; + this.forceSlash = forceSlash; } public DirectoriesCompleter(Supplier currentDir) { + this(currentDir, false); + } + + public DirectoriesCompleter(Supplier currentDir, boolean forceSlash) { this.currentDir = currentDir; + this.forceSlash = forceSlash; } @Override @@ -237,6 +252,11 @@ protected Path getUserDir() { return currentDir.get(); } + @Override + protected String getSeparator() { + return forceSlash ? "/" : getUserDir().getFileSystem().getSeparator(); + } + @Override protected boolean accept(Path path) { return Files.isDirectory(path) && super.accept(path); @@ -246,23 +266,43 @@ protected boolean accept(Path path) { public static class FilesCompleter extends FileNameCompleter { private final Supplier currentDir; + private final boolean forceSlash; public FilesCompleter(File currentDir) { - this(currentDir.toPath()); + this(currentDir.toPath(), false); + } + + public FilesCompleter(File currentDir, boolean forceSlash) { + this(currentDir.toPath(), forceSlash); } public FilesCompleter(Path currentDir) { + this(currentDir, false); + } + + public FilesCompleter(Path currentDir, boolean forceSlash) { this.currentDir = () -> currentDir; + this.forceSlash = forceSlash; } public FilesCompleter(Supplier currentDir) { + this(currentDir, false); + } + + public FilesCompleter(Supplier currentDir, boolean forceSlash) { this.currentDir = currentDir; + this.forceSlash = forceSlash; } @Override protected Path getUserDir() { return currentDir.get(); } + + @Override + protected String getSeparator() { + return forceSlash ? "/" : getUserDir().getFileSystem().getSeparator(); + } } /** @@ -295,7 +335,7 @@ public void complete(LineReader reader, ParsedLine commandLine, final List= 0) { curBuf = buffer.substring(0, lastSep + 1); @@ -345,6 +385,10 @@ protected Path getUserDir() { protected Path getUserHome() { return Paths.get(System.getProperty("user.home")); } + + protected String getSeparator() { + return getUserDir().getFileSystem().getSeparator(); + } protected String getDisplay(Terminal terminal, Path p) { // TODO: use $LS_COLORS for output diff --git a/builtins/src/main/java/org/jline/builtins/Less.java b/builtins/src/main/java/org/jline/builtins/Less.java index e395cfc0b..697d19379 100644 --- a/builtins/src/main/java/org/jline/builtins/Less.java +++ b/builtins/src/main/java/org/jline/builtins/Less.java @@ -9,11 +9,16 @@ package org.jline.builtins; import java.io.BufferedReader; +import java.io.FileNotFoundException; import java.io.FilterInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.InterruptedIOException; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.PathMatcher; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -24,6 +29,7 @@ import java.util.regex.PatternSyntaxException; import org.jline.builtins.Source.ResourceSource; +import org.jline.builtins.Source.URLSource; import org.jline.keymap.BindingReader; import org.jline.keymap.KeyMap; import org.jline.terminal.Attributes; @@ -65,6 +71,7 @@ public class Less { protected final Terminal terminal; protected final Display display; protected final BindingReader bindingReader; + protected final Path currentDir; protected List sources; protected int sourceIdx; @@ -96,10 +103,11 @@ public class Less { protected final Size size = new Size(); - public Less(Terminal terminal) { + public Less(Terminal terminal, Path currentDir) { this.terminal = terminal; this.display = new Display(terminal, true); this.bindingReader = new BindingReader(terminal.reader()); + this.currentDir = currentDir; } public Less tabs(List tabs) { @@ -248,6 +256,10 @@ else if (buffer.length() > 0 && (buffer.charAt(0) == '/' || buffer.charAt(0) == buffer.setLength(0); } buffer.append(c); + } else if (obj == Operation.BACKSPACE) { + if (buffer.length() > 0) { + buffer.deleteCharAt(buffer.length() - 1); + } } else { op = obj; } @@ -362,11 +374,20 @@ else if (buffer.length() > 0 && (buffer.charAt(0) == '/' || buffer.charAt(0) == ignoreCaseCond = false; message = ignoreCaseAlways ? "Ignore case in searches and in patterns" : "Case is significant in searches"; break; + case ADD_FILE: + addFile(); + break; case NEXT_FILE: int next = getStrictPositiveNumberInBuffer(1); if (sourceIdx < sources.size() - next) { + SavedSourcePositions ssp = new SavedSourcePositions(); sourceIdx += next; - openSource(); + String newSource = sources.get(sourceIdx).getName(); + try { + openSource(); + } catch (FileNotFoundException exp) { + ssp.restore(newSource); + } } else { message = "No next file"; } @@ -374,8 +395,14 @@ else if (buffer.length() > 0 && (buffer.charAt(0) == '/' || buffer.charAt(0) == case PREV_FILE: int prev = getStrictPositiveNumberInBuffer(1); if (sourceIdx > prev) { + SavedSourcePositions ssp = new SavedSourcePositions(-1); sourceIdx -= prev; - openSource(); + String newSource = sources.get(sourceIdx).getName(); + try { + openSource(); + } catch (FileNotFoundException exp) { + ssp.restore(newSource); + } } else { message = "No previous file"; } @@ -383,8 +410,14 @@ else if (buffer.length() > 0 && (buffer.charAt(0) == '/' || buffer.charAt(0) == case GOTO_FILE: int tofile = getStrictPositiveNumberInBuffer(1); if (tofile < sources.size()) { + SavedSourcePositions ssp = new SavedSourcePositions(tofile < sourceIdx ? -1 : 0); sourceIdx = tofile; - openSource(); + String newSource = sources.get(sourceIdx).getName(); + try { + openSource(); + } catch (FileNotFoundException exp) { + ssp.restore(newSource); + } } else { message = "No such file"; } @@ -401,6 +434,15 @@ else if (buffer.length() > 0 && (buffer.charAt(0) == '/' || buffer.charAt(0) == openSource(); } break; + case REPAINT: + size.copy(terminal.getSize()); + display.clear(); + break; + case REPAINT_AND_DISCARD: + message = null; + size.copy(terminal.getSize()); + display.clear(); + break; case HELP: help(); break; @@ -434,47 +476,29 @@ else if (buffer.length() > 0 && (buffer.charAt(0) == '/' || buffer.charAt(0) == terminal.writer().flush(); } } finally { - reader.close(); + if (reader != null) { + reader.close(); + } if (status != null) { status.restore(); } } } - private boolean search() throws IOException, InterruptedException { - KeyMap searchKeyMap = new KeyMap<>(); - searchKeyMap.setUnicode(Operation.INSERT); - for (char i = 32; i < 256; i++) { - searchKeyMap.bind(Operation.INSERT, Character.toString(i)); + private class LineEditor { + private int begPos; + + public LineEditor(int begPos) { + this.begPos = begPos; } - searchKeyMap.bind(Operation.RIGHT, key(terminal, Capability.key_right), alt('l')); - searchKeyMap.bind(Operation.LEFT, key(terminal, Capability.key_left), alt('h')); - searchKeyMap.bind(Operation.NEXT_WORD, alt('w')); - searchKeyMap.bind(Operation.PREV_WORD, alt('b')); - searchKeyMap.bind(Operation.HOME, key(terminal, Capability.key_home), alt('0')); - searchKeyMap.bind(Operation.END, key(terminal, Capability.key_end), alt('$')); - searchKeyMap.bind(Operation.BACKSPACE, del()); - searchKeyMap.bind(Operation.DELETE, alt('x')); - searchKeyMap.bind(Operation.DELETE_WORD, alt('X')); - searchKeyMap.bind(Operation.DELETE_LINE, ctrl('U')); - searchKeyMap.bind(Operation.UP, key(terminal, Capability.key_up), alt('k')); - searchKeyMap.bind(Operation.DOWN, key(terminal, Capability.key_down), alt('j')); - searchKeyMap.bind(Operation.ACCEPT, "\r"); - boolean forward = true; - message = null; - int curPos = buffer.length(); - final int begPos = curPos; - final char type = buffer.charAt(0); - String currentBuffer = ""; - while (true) { - checkInterrupted(); - switch (bindingReader.readBinding(searchKeyMap)) { + public int editBuffer(Operation op, int curPos) { + switch (op) { case INSERT: buffer.insert(curPos++, bindingReader.getLastBinding()); break; case BACKSPACE: - if (curPos > begPos) { + if (curPos > begPos - 1) { buffer.deleteCharAt(--curPos); } break; @@ -531,7 +555,7 @@ private boolean search() throws IOException, InterruptedException { } break; case DELETE_LINE: - buffer.setLength(1); + buffer.setLength(begPos); curPos = 1; break; case LEFT: @@ -544,6 +568,133 @@ private boolean search() throws IOException, InterruptedException { curPos++; } break; + } + return curPos; + } + } + + private class SavedSourcePositions { + int saveSourceIdx; + int saveFirstLineToDisplay; + int saveFirstColumnToDisplay; + int saveOffsetInLine; + boolean savePrintLineNumbers; + + public SavedSourcePositions (){ + this(0); + } + public SavedSourcePositions (int dec){ + saveSourceIdx = sourceIdx + dec; + saveFirstLineToDisplay = firstLineToDisplay; + saveFirstColumnToDisplay = firstColumnToDisplay; + saveOffsetInLine = offsetInLine; + savePrintLineNumbers = printLineNumbers; + } + + public void restore(String failingSource) throws IOException { + sourceIdx = saveSourceIdx; + openSource(); + firstLineToDisplay = saveFirstLineToDisplay; + firstColumnToDisplay = saveFirstColumnToDisplay; + offsetInLine = saveOffsetInLine; + printLineNumbers = savePrintLineNumbers; + if (failingSource != null) { + message = failingSource + " not found!"; + } + } + } + + private void addSource(String file) throws IOException { + if (file.contains("*") || file.contains("?")) { + PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:"+file); + Files.find(currentDir, Integer.MAX_VALUE, (path, f) -> pathMatcher.matches(path)) + .forEach(p -> sources.add(Commands.doUrlSource(currentDir, p))); + } else { + sources.add(new URLSource(currentDir.resolve(file).toUri().toURL(), file)); + } + sourceIdx = sources.size() - 1; + } + + private void addFile() throws IOException, InterruptedException { + KeyMap fileKeyMap = new KeyMap<>(); + fileKeyMap.setUnicode(Operation.INSERT); + for (char i = 32; i < 256; i++) { + fileKeyMap.bind(Operation.INSERT, Character.toString(i)); + } + fileKeyMap.bind(Operation.RIGHT, key(terminal, Capability.key_right), alt('l')); + fileKeyMap.bind(Operation.LEFT, key(terminal, Capability.key_left), alt('h')); + fileKeyMap.bind(Operation.HOME, key(terminal, Capability.key_home), alt('0')); + fileKeyMap.bind(Operation.END, key(terminal, Capability.key_end), alt('$')); + fileKeyMap.bind(Operation.BACKSPACE, del()); + fileKeyMap.bind(Operation.DELETE, alt('x')); + fileKeyMap.bind(Operation.DELETE_WORD, alt('X')); + fileKeyMap.bind(Operation.DELETE_LINE, ctrl('U')); + fileKeyMap.bind(Operation.ACCEPT, "\r"); + + SavedSourcePositions ssp = new SavedSourcePositions(); + message = null; + buffer.append("Examine: "); + int curPos = buffer.length(); + final int begPos = curPos; + display(false, curPos); + LineEditor lineEditor = new LineEditor(begPos); + while (true) { + checkInterrupted(); + Operation op = null; + switch (op=bindingReader.readBinding(fileKeyMap)) { + case ACCEPT: + String name = buffer.toString().substring(begPos); + addSource(name); + try { + openSource(); + } catch (Exception exp) { + ssp.restore(name); + } + return; + default: + curPos = lineEditor.editBuffer(op, curPos); + break; + } + if (curPos > begPos) { + display(false, curPos); + } else { + buffer.setLength(0); + return; + } + } + } + + private boolean search() throws IOException, InterruptedException { + KeyMap searchKeyMap = new KeyMap<>(); + searchKeyMap.setUnicode(Operation.INSERT); + for (char i = 32; i < 256; i++) { + searchKeyMap.bind(Operation.INSERT, Character.toString(i)); + } + searchKeyMap.bind(Operation.RIGHT, key(terminal, Capability.key_right), alt('l')); + searchKeyMap.bind(Operation.LEFT, key(terminal, Capability.key_left), alt('h')); + searchKeyMap.bind(Operation.NEXT_WORD, alt('w')); + searchKeyMap.bind(Operation.PREV_WORD, alt('b')); + searchKeyMap.bind(Operation.HOME, key(terminal, Capability.key_home), alt('0')); + searchKeyMap.bind(Operation.END, key(terminal, Capability.key_end), alt('$')); + searchKeyMap.bind(Operation.BACKSPACE, del()); + searchKeyMap.bind(Operation.DELETE, alt('x')); + searchKeyMap.bind(Operation.DELETE_WORD, alt('X')); + searchKeyMap.bind(Operation.DELETE_LINE, ctrl('U')); + searchKeyMap.bind(Operation.UP, key(terminal, Capability.key_up), alt('k')); + searchKeyMap.bind(Operation.DOWN, key(terminal, Capability.key_down), alt('j')); + searchKeyMap.bind(Operation.ACCEPT, "\r"); + + boolean forward = true; + message = null; + int curPos = buffer.length(); + final int begPos = curPos; + final char type = buffer.charAt(0); + String currentBuffer = ""; + LineEditor lineEditor = new LineEditor(begPos); + while (true) { + checkInterrupted(); + Operation op = null; + switch (op=bindingReader.readBinding(searchKeyMap)) { case UP: patternId++; if (patternId >= 0 && patternId < patterns.size()) { @@ -615,18 +766,21 @@ private boolean search() throws IOException, InterruptedException { message = null; } return forward; + default: + curPos = lineEditor.editBuffer(op, curPos); + break; + } + if (curPos < begPos) { + buffer.setLength(0); + return forward; + } else { + display(false, curPos); } - display(false, curPos); } } private void help() throws IOException { - int saveSourceIdx = sourceIdx; - int saveFirstLineToDisplay = firstLineToDisplay; - int saveFirstColumnToDisplay = firstColumnToDisplay; - int saveOffsetInLine = offsetInLine; - boolean savePrintLineNumbers = printLineNumbers; - + SavedSourcePositions ssp = new SavedSourcePositions(); printLineNumbers = false; sourceIdx = 0; try { @@ -635,7 +789,6 @@ private void help() throws IOException { Operation op = null; do { checkInterrupted(); - op = bindingReader.readBinding(keys, null, false); if (op != null) { switch (op) { @@ -652,32 +805,65 @@ private void help() throws IOException { } catch (IOException|InterruptedException exp) { // Do nothing } finally { - sourceIdx = saveSourceIdx; - openSource(); - firstLineToDisplay = saveFirstLineToDisplay; - firstColumnToDisplay = saveFirstColumnToDisplay; - offsetInLine = saveOffsetInLine; - printLineNumbers = savePrintLineNumbers; + ssp.restore(null); } } - + protected void openSource() throws IOException { + boolean wasOpen = false; if (reader != null) { reader.close(); + wasOpen = true; } - Source source = sources.get(sourceIdx); - InputStream in = source.read(); - if (sources.size() == 2 || sourceIdx == 0) { - message = source.getName(); - } else { - message = source.getName() + " (file " + sourceIdx + " of " + (sources.size() - 1)+ ")"; + boolean open = false; + boolean displayMessage = false; + do { + Source source = sources.get(sourceIdx); + try { + InputStream in = source.read(); + if (sources.size() == 2 || sourceIdx == 0) { + message = source.getName(); + } else { + message = source.getName() + " (file " + sourceIdx + " of " + + (sources.size() - 1) + ")"; + } + reader = new BufferedReader(new InputStreamReader( + new InterruptibleInputStream(in))); + firstLineInMemory = 0; + lines = new ArrayList<>(); + firstLineToDisplay = 0; + firstColumnToDisplay = 0; + offsetInLine = 0; + display.clear(); + open = true; + if (displayMessage) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.style(AttributedStyle.INVERSE); + asb.append(source.getName() + " (press RETURN)"); + asb.toAttributedString().println(terminal); + terminal.writer().flush(); + terminal.reader().read(); + } + } catch (FileNotFoundException exp) { + sources.remove(sourceIdx); + if (sourceIdx > sources.size() - 1) { + sourceIdx = sources.size() - 1; + } + if (wasOpen) { + throw exp; + } else { + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.append(source.getName() + " not found!"); + asb.toAttributedString().println(terminal); + terminal.writer().flush(); + open = false; + displayMessage = true; + } + } + } while (!open && sourceIdx > 0); + if (!open) { + throw new FileNotFoundException(); } - reader = new BufferedReader(new InputStreamReader(new InterruptibleInputStream(in))); - firstLineInMemory = 0; - lines = new ArrayList<>(); - firstLineToDisplay = 0; - firstColumnToDisplay = 0; - offsetInLine = 0; } void moveTo(int lineNum) throws IOException { @@ -1031,6 +1217,8 @@ private void bindKeys(KeyMap map) { map.bind(Operation.RIGHT_ONE_HALF_SCREEN, alt(')'), key(terminal, Capability.key_right)); map.bind(Operation.LEFT_ONE_HALF_SCREEN, alt('('), key(terminal, Capability.key_left)); map.bind(Operation.FORWARD_FOREVER, "F"); + map.bind(Operation.REPAINT, "r", ctrl('R'), ctrl('L')); + map.bind(Operation.REPAINT_AND_DISCARD, "R"); map.bind(Operation.REPEAT_SEARCH_FORWARD, "n"); map.bind(Operation.REPEAT_SEARCH_BACKWARD, "N"); map.bind(Operation.REPEAT_SEARCH_FORWARD_SPAN_FILES, alt('n')); @@ -1040,11 +1228,13 @@ private void bindKeys(KeyMap map) { map.bind(Operation.GO_TO_LAST_LINE_OR_N, "G", ">", alt('>')); map.bind(Operation.HOME, key(terminal, Capability.key_home)); map.bind(Operation.END, key(terminal, Capability.key_end)); + map.bind(Operation.ADD_FILE, ":e", ctrl('X') + ctrl('V')); map.bind(Operation.NEXT_FILE, ":n"); map.bind(Operation.PREV_FILE, ":p"); map.bind(Operation.GOTO_FILE, ":x"); map.bind(Operation.INFO_FILE, "=", ":f", ctrl('G')); map.bind(Operation.DELETE_FILE, ":d"); + map.bind(Operation.BACKSPACE, del()); "-/0123456789?&".chars().forEach(c -> map.bind(Operation.CHAR, Character.toString((char) c))); } @@ -1097,6 +1287,7 @@ protected enum Operation { OPT_IGNORE_CASE_ALWAYS, // Files + ADD_FILE, NEXT_FILE, PREV_FILE, GOTO_FILE, diff --git a/builtins/src/main/java/org/jline/builtins/Nano.java b/builtins/src/main/java/org/jline/builtins/Nano.java index 957210284..687062534 100644 --- a/builtins/src/main/java/org/jline/builtins/Nano.java +++ b/builtins/src/main/java/org/jline/builtins/Nano.java @@ -86,7 +86,7 @@ public class Nano { public boolean mouseSupport = false; public boolean oneMoreLine = true; public boolean constantCursor; - public int tabs = 4; + public int tabs = 1; // tabs are not currently supported! public String brackets = "\"’)>]}"; public String matchBrackets = "(<[{)>]}"; public String punct = "!.?"; @@ -114,6 +114,9 @@ public class Nano { protected int searchTermId = -1; protected WriteMode writeMode = WriteMode.WRITE; protected boolean writeBackup; + protected List cutbuffer = new ArrayList<>(); + protected boolean cut2end = false; + protected boolean mark = false; protected boolean readNewBuffer = true; @@ -136,7 +139,7 @@ protected class Buffer { List lines; int firstLineToDisplay; - int firstColumnToDisplay; + int firstColumnToDisplay = 0; int offsetInLineToDisplay; int line; @@ -144,11 +147,15 @@ protected class Buffer { int offsetInLine; int column; int wantedColumn; + boolean uncut = false; + int[] markPos = {-1, -1}; // line, offsetInLine + column + SyntaxHighlighter syntaxHighlighter; boolean dirty; protected Buffer(String file) { this.file = file; + this.syntaxHighlighter = doSyntaxHighlighter(); } void open() throws IOException { @@ -256,6 +263,7 @@ void insert(String insert) { offsets.add(line, computeOffsets(ins.get(i))); } moveToChar(ins.get(ins.size() - 1).length() - (text.length() - pos)); + ensureCursorVisible(); dirty = true; } @@ -270,22 +278,24 @@ LinkedList computeOffsets(String text) { int width = size.getColumns() - (printLineNumbers ? 8 : 0); LinkedList offsets = new LinkedList<>(); offsets.add(0); - int last = 0; - int prevword = 0; - boolean inspace = false; - for (int i = 0; i < text.length(); i++) { - if (isBreakable(text.charAt(i))) { - inspace = true; - } else if (inspace) { - prevword = i; - inspace = false; - } - if (i == last + width - 1) { - if (prevword == last) { + if (wrapping) { + int last = 0; + int prevword = 0; + boolean inspace = false; + for (int i = 0; i < text.length(); i++) { + if (isBreakable(text.charAt(i))) { + inspace = true; + } else if (inspace) { prevword = i; + inspace = false; + } + if (i == last + width - 1) { + if (prevword == last) { + prevword = i; + } + offsets.add(prevword); + last = prevword; } - offsets.add(prevword); - last = prevword; } } return offsets; @@ -296,6 +306,13 @@ boolean isBreakable(char ch) { } void moveToChar(int pos) { + if (!wrapping) { + if (pos > column && pos - firstColumnToDisplay + 1 > width()) { + firstColumnToDisplay = offsetInLine + column - 6; + } else if (pos < column && firstColumnToDisplay + 5 > pos) { + firstColumnToDisplay = Math.max(0, firstColumnToDisplay - width() + 5); + } + } offsetInLine = prevLineOffset(line, pos + 1).get(); column = pos - offsetInLine; } @@ -331,6 +348,7 @@ boolean backspace(int count) { dirty = true; } } + ensureCursorVisible(); return true; } @@ -354,13 +372,28 @@ boolean moveLeft(int chars) { } boolean moveRight(int chars) { + return moveRight(chars, false); + } + + int width() { + return size.getColumns() - (printLineNumbers ? 8 : 0) - (wrapping ? 0 : 1) - (firstColumnToDisplay > 0 ? 1 : 0); + } + + boolean moveRight(int chars, boolean fromBeginning) { + if (fromBeginning) { + firstColumnToDisplay = 0; + offsetInLine = 0; + column = 0; + chars = chars <= length(getLine(line), tabs) ? chars : length(getLine(line), tabs); + } boolean ret = true; while (--chars >= 0) { - int len = length(getLine(line), tabs); + int len = length(getLine(line), tabs); if (offsetInLine + column + 1 <= len) { moveToChar(offsetInLine + column + 1); } else if (getLine(line + 1) != null) { line++; + firstColumnToDisplay = 0; offsetInLine = 0; column = 0; } else { @@ -412,7 +445,7 @@ void moveDisplayDown(int lines) { // Adjust cursor while (--lines >= 0) { int lastLineToDisplay = firstLineToDisplay; - if (firstColumnToDisplay > 0 || !wrapping) { + if (!wrapping) { lastLineToDisplay += height - 1; } else { int off = offsetInLineToDisplay; @@ -457,8 +490,9 @@ void moveDisplayUp(int lines) { private void cursorDown(int lines) { // Adjust cursor + firstColumnToDisplay = 0; while (--lines >= 0) { - if (firstColumnToDisplay > 0 || !wrapping) { + if (!wrapping) { if (getLine(line + 1) != null) { line++; offsetInLine = 0; @@ -488,8 +522,9 @@ private void cursorDown(int lines) { } private void cursorUp(int lines) { + firstColumnToDisplay = 0; while (--lines >= 0) { - if (firstColumnToDisplay > 0 || !wrapping) { + if (!wrapping) { if (line > 0) { line--; column = Math.min(length(getLine(line), tabs) - offsetInLine, wantedColumn); @@ -525,31 +560,7 @@ void ensureCursorVisible() { } while (true) { - int cursor = header.size() * size.getColumns() + (printLineNumbers ? 8 : 0); - int cur = firstLineToDisplay; - int off = offsetInLineToDisplay; - while (true) { - if (cur < line || off < offsetInLine) { - if (firstColumnToDisplay > 0 || !wrapping) { - cursor += rwidth; - cur++; - } else { - cursor += rwidth; - Optional next = nextLineOffset(cur, off); - if (next.isPresent()) { - off = next.get(); - } else { - cur++; - off = 0; - } - } - } else if (cur == line) { - cursor += column; - break; - } else { - throw new IllegalStateException(); - } - } + int cursor = computeCursorPosition(header.size() * size.getColumns() + (printLineNumbers ? 8 : 0), rwidth); if (cursor >= (height + header.size()) * rwidth) { moveDisplayDown(smoothScrolling ? 1 : height / 2); } else { @@ -565,10 +576,13 @@ void bof() { } void resetDisplay() { - int width = size.getColumns() - (printLineNumbers ? 8 : 0); +// mrn 29/7/2019 same calculation as in endOfLine() & nextSearch() methods that were failing +// change also here although haven't seen an error +// int width = size.getColumns() - (printLineNumbers ? 8 : 0); column = offsetInLine + column; - offsetInLine = (column / width) * (width - 1); - column = column - offsetInLine; +// offsetInLine = (column / width) * (width - 1); +// column = column - offsetInLine; + moveRight(column, true); } String getLine(int line) { @@ -652,6 +666,109 @@ List computeHeader() { } } + void highlightDisplayedLine(int curLine, int curOffset, int nextOffset, AttributedStringBuilder line){ + AttributedString disp = syntaxHighlighter.highlightNextLine(new AttributedString(getLine(curLine))); + if (!mark) { + line.append(disp.columnSubSequence(curOffset, nextOffset)); + } else if (getMarkStart()[0] == getMarkEnd()[0]) { + if (curLine == getMarkStart()[0]) { + if (getMarkStart()[1] > nextOffset) { + line.append(disp.columnSubSequence(curOffset, nextOffset)); + } else if (getMarkStart()[1] < curOffset) { + if (getMarkEnd()[1] > nextOffset) { + line.append(disp.columnSubSequence(curOffset, nextOffset), AttributedStyle.INVERSE); + } else if (getMarkEnd()[1] > curOffset) { + line.append(disp.columnSubSequence(curOffset, getMarkEnd()[1]), AttributedStyle.INVERSE); + line.append(disp.columnSubSequence(getMarkEnd()[1], nextOffset)); + } else { + line.append(disp.columnSubSequence(curOffset, nextOffset)); + } + } else { + line.append(disp.columnSubSequence(curOffset, getMarkStart()[1])); + if (getMarkEnd()[1] > nextOffset) { + line.append(disp.columnSubSequence(getMarkStart()[1], nextOffset), AttributedStyle.INVERSE); + } else { + line.append(disp.columnSubSequence(getMarkStart()[1], getMarkEnd()[1]), AttributedStyle.INVERSE); + line.append(disp.columnSubSequence(getMarkEnd()[1], nextOffset)); + } + } + } else { + line.append(disp.columnSubSequence(curOffset, nextOffset)); + } + } else { + if (curLine > getMarkStart()[0] && curLine < getMarkEnd()[0]) { + line.append(disp.columnSubSequence(curOffset, nextOffset), AttributedStyle.INVERSE); + } else if (curLine == getMarkStart()[0]) { + if (getMarkStart()[1] > nextOffset) { + line.append(disp.columnSubSequence(curOffset, nextOffset)); + } else if (getMarkStart()[1] < curOffset) { + line.append(disp.columnSubSequence(curOffset, nextOffset), AttributedStyle.INVERSE); + } else { + line.append(disp.columnSubSequence(curOffset, getMarkStart()[1])); + line.append(disp.columnSubSequence(getMarkStart()[1], nextOffset), AttributedStyle.INVERSE); + } + } else if (curLine == getMarkEnd()[0]) { + if (getMarkEnd()[1] < curOffset) { + line.append(disp.columnSubSequence(curOffset, nextOffset)); + } else if (getMarkEnd()[1] > nextOffset) { + line.append(disp.columnSubSequence(curOffset, nextOffset), AttributedStyle.INVERSE); + } else { + line.append(disp.columnSubSequence(curOffset, getMarkEnd()[1]), AttributedStyle.INVERSE); + line.append(disp.columnSubSequence(getMarkEnd()[1], nextOffset)); + } + } else { + line.append(disp.columnSubSequence(curOffset, nextOffset)); + } + } + } + + /* + * Hardcoded syntax patterns... these should be read from config file + * Patterns could be get from + * https://github.com/scopatz/nanorc + * and converted prefibilmente automatically to java format when reading + * config + */ + private SyntaxHighlighter doSyntaxHighlighter() { + AttributedStyle s = AttributedStyle.DEFAULT.foreground(AttributedStyle.BLACK + AttributedStyle.BRIGHT); + SyntaxHighlighter out = new SyntaxHighlighter(); + if (file == null) { + // do nothing + } else if (file.endsWith(".java")) { + out.addRule(s.foreground(AttributedStyle.GREEN) + , Pattern.compile("\\b(boolean|byte|char|double|float|int|long|new|short|this" + + "|transient|void)\\b")); + out.addRule(s.foreground(AttributedStyle.RED) + , Pattern.compile("\\b(break|case|catch|continue|default|do|else|finally|for" + + "|if|return|switch|throw|try|while)\\b")); + out.addRule(s.foreground(AttributedStyle.CYAN) + , Pattern.compile("\\b(abstract|class|extends|final|implements|import|instanceof" + + "|interface|native|package|private|protected|public|static|strictfp" + + "|super|synchronized|throws|volatile)\\b")); + out.addRule(s.foreground(AttributedStyle.RED), Pattern.compile("\"[^\"]*\"")); + out.addRule(s.foreground(AttributedStyle.YELLOW), Pattern.compile("\\b(true|false|null)\\b")); + out.addRule(s.foreground(AttributedStyle.YELLOW), Pattern.compile("\\b(([1-9][0-9]+)|0+)\\.[0-9]+\\b")); + out.addRule(s.foreground(AttributedStyle.YELLOW), Pattern.compile("\\b[1-9][0-9]*\\b")); + out.addRule(s.foreground(AttributedStyle.YELLOW), Pattern.compile("\\b0[0-7]*\\b")); + out.addRule(s.foreground(AttributedStyle.YELLOW), Pattern.compile("\\b0x[1-9a-f][0-9a-f]*\\b")); + out.addRule(s.foreground(AttributedStyle.BLUE), Pattern.compile("//.*")); + out.addRule(s.foreground(AttributedStyle.BLUE), Pattern.compile("/\\*"), Pattern.compile("\\*/")); + out.addRule(s.foreground(AttributedStyle.BLUE + AttributedStyle.BRIGHT), Pattern.compile("/\\*\\*"), Pattern.compile("\\*/")); + out.addRule(s.background(AttributedStyle.GREEN), Pattern.compile("\\s+$")); + } else if (file.endsWith(".xml")) { + out.addRule(s.foreground(AttributedStyle.WHITE), Pattern.compile("^.+$")); + out.addRule(s.foreground(AttributedStyle.GREEN), Pattern.compile("<"), Pattern.compile(">")); + out.addRule(s.foreground(AttributedStyle.CYAN), Pattern.compile("<[^> ]+")); + out.addRule(s.foreground(AttributedStyle.MAGENTA), Pattern.compile("\"[^\"]*\"")); + out.addRule(s.foreground(AttributedStyle.CYAN), Pattern.compile(">")); + out.addRule(s.foreground(AttributedStyle.YELLOW), Pattern.compile("")); + out.addRule(s.foreground(AttributedStyle.YELLOW), Pattern.compile("")); + out.addRule(s.foreground(AttributedStyle.RED), Pattern.compile("&[^;]*;")); + out.addRule(s.background(AttributedStyle.GREEN), Pattern.compile("\\s+$")); + } + return out; + } + List getDisplayedLines(int nbLines) { AttributedStyle s = AttributedStyle.DEFAULT.foreground(AttributedStyle.BLACK + AttributedStyle.BRIGHT); AttributedString cut = new AttributedString("…", s); @@ -663,6 +780,7 @@ List getDisplayedLines(int nbLines) { int curLine = firstLineToDisplay; int curOffset = offsetInLineToDisplay; int prevLine = -1; + syntaxHighlighter.reset(); for (int terminalLine = 0; terminalLine < nbLines; terminalLine++) { AttributedStringBuilder line = new AttributedStringBuilder().tabs(tabs); if (printLineNumbers && curLine < lines.size()) { @@ -677,26 +795,38 @@ List getDisplayedLines(int nbLines) { } if (curLine >= lines.size()) { // Nothing to do - } else if (firstColumnToDisplay > 0 || !wrapping) { + } else if (!wrapping) { AttributedString disp = new AttributedString(getLine(curLine)); - disp = disp.columnSubSequence(firstColumnToDisplay, Integer.MAX_VALUE); - if (disp.columnLength() >= width) { - line.append(disp.columnSubSequence(0, width - cut.columnLength())); - line.append(cut); + if (this.line == curLine) { + int cutCount = 1; + if (firstColumnToDisplay > 0) { + line.append(cut); + cutCount = 2; + } + if (disp.columnLength() - firstColumnToDisplay >= width - (cutCount - 1)*cut.columnLength()) { + highlightDisplayedLine(curLine, firstColumnToDisplay + , firstColumnToDisplay + width - cutCount*cut.columnLength(), line); + line.append(cut); + } else { + highlightDisplayedLine(curLine, firstColumnToDisplay, disp.columnLength(), line); + } } else { - line.append(disp); + if (disp.columnLength() >= width) { + highlightDisplayedLine(curLine, 0, width - cut.columnLength(), line); + line.append(cut); + } else { + highlightDisplayedLine(curLine, 0, disp.columnLength(), line); + } } curLine++; } else { Optional nextOffset = nextLineOffset(curLine, curOffset); if (nextOffset.isPresent()) { - AttributedString disp = new AttributedString(getLine(curLine)); - line.append(disp.columnSubSequence(curOffset, nextOffset.get())); + highlightDisplayedLine(curLine, curOffset, nextOffset.get(), line); line.append(ret); curOffset = nextOffset.get(); } else { - AttributedString disp = new AttributedString(getLine(curLine)); - line.append(disp.columnSubSequence(curOffset, Integer.MAX_VALUE)); + highlightDisplayedLine(curLine, curOffset, Integer.MAX_VALUE, line); curLine++; curOffset = 0; } @@ -717,14 +847,26 @@ public void moveTo(int x, int y) { cursorDown(y); } + public void gotoLine(int x, int y) { + line = y < lines.size() ? y : lines.size() - 1; + x = x <= length(lines.get(line), tabs) ? x : length(lines.get(line), tabs); + firstLineToDisplay = line > 0 ? line - 1 : line; + offsetInLine = 0; + offsetInLineToDisplay = 0; + column = 0; + moveRight(x); + } + public int getDisplayedCursor() { - int rwidth = size.getColumns() + 1; - int cursor = (printLineNumbers ? 8 : 0); + return computeCursorPosition(printLineNumbers ? 8 : 0, size.getColumns() + 1); + } + + private int computeCursorPosition(int cursor, int rwidth) { int cur = firstLineToDisplay; int off = offsetInLineToDisplay; while (true) { if (cur < line || off < offsetInLine) { - if (firstColumnToDisplay > 0 || !wrapping) { + if (!wrapping) { cursor += rwidth; cur++; } else { @@ -738,7 +880,12 @@ public int getDisplayedCursor() { } } } else if (cur == line) { - cursor += column; + if (!wrapping && column > firstColumnToDisplay + width()) { + while (column > firstColumnToDisplay + width()) { + firstColumnToDisplay += width(); + } + } + cursor += column - firstColumnToDisplay + (firstColumnToDisplay > 0 ? 1 : 0); break; } else { throw new IllegalStateException(); @@ -780,24 +927,28 @@ public void nextWord() { public void beginningOfLine() { column = offsetInLine = 0; wantedColumn = 0; + ensureCursorVisible(); } public void endOfLine() { - column = length(lines.get(line), tabs); - int width = size.getColumns() - (printLineNumbers ? 8 : 0); - offsetInLine = (column / width) * (width - 1); - column = column - offsetInLine; - wantedColumn = column; + int x = length(lines.get(line), tabs); + moveRight(x, true); } public void prevPage() { int height = size.getRows() - computeHeader().size() - computeFooter().size(); scrollUp(height - 2); + column = 0; + firstLineToDisplay = line; + offsetInLineToDisplay = offsetInLine; } public void nextPage() { int height = size.getRows() - computeHeader().size() - computeFooter().size(); scrollDown(height - 2); + column = 0; + firstLineToDisplay = line; + offsetInLineToDisplay = offsetInLine; } public void scrollUp(int lines) { @@ -874,11 +1025,8 @@ void nextSearch() { || (!searchBackwards && (newLine < line || (newLine == line && newPos < offsetInLine + column)))) { setMessage("Search Wrapped"); } - int width = size.getColumns() - (printLineNumbers ? 8 : 0); line = newLine; - column = newPos; - offsetInLine = (column / width) * (width - 1); - ensureCursorVisible(); + moveRight(newPos, true); } else { setMessage("\"" + searchTerm + "\" not found"); } @@ -940,6 +1088,326 @@ public void matching() { private int length(String line, int tabs) { return new AttributedStringBuilder().tabs(tabs).append(line).columnLength(); } + + void copy() { + if (uncut || cut2end || mark) { + cutbuffer = new ArrayList<>(); + } + if (mark) { + int[] s = getMarkStart(); + int[] e = getMarkEnd(); + if (s[0] == e[0]) { + cutbuffer.add(lines.get(s[0]).substring(s[1], e[1])); + } else { + if (s[1] != 0) { + cutbuffer.add(lines.get(s[0]).substring(s[1])); + s[0] = s[0] + 1; + } + for (int i = s[0]; i < e[0]; i++) { + cutbuffer.add(lines.get(i)); + } + if (e[1] != 0) { + cutbuffer.add(lines.get(e[0]).substring(0, e[1])); + } + } + mark = false; + mark(); + } else if (cut2end) { + String l = lines.get(line); + int col = offsetInLine + column; + cutbuffer.add(l.substring(col)); + moveRight(l.substring(col).length()); + } else { + cutbuffer.add(lines.get(line)); + cursorDown(1); + } + uncut = false; + } + + void cut() { + cut(false); + } + + void cut(boolean toEnd) { + if (lines.size() > 1) { + if (uncut || cut2end || toEnd || mark) { + cutbuffer = new ArrayList<>(); + } + if (mark) { + int[] s = getMarkStart(); + int[] e = getMarkEnd(); + if (s[0] == e[0]) { + String l = lines.get(s[0]); + int cols = s[1]; + int cole = e[1]; + cutbuffer.add(l.substring(cols, cole)); + lines.set(s[0], l.substring(0, cols) + l.substring(cole)); + computeAllOffsets(); + moveRight(cols, true); + } else { + int ls = s[0]; + int cs = s[1]; + if (s[1] != 0) { + String l = lines.get(s[0]); + int col = s[1]; + cutbuffer.add(l.substring(col)); + lines.set(s[0], l.substring(0, col)); + s[0] = s[0] + 1; + } + for (int i = s[0]; i < e[0]; i++) { + cutbuffer.add(lines.get(s[0])); + lines.remove(s[0]); + } + if (e[1] != 0) { + String l = lines.get(s[0]); + int col = e[1]; + cutbuffer.add(l.substring(0, col)); + lines.set(s[0], l.substring(col)); + } + computeAllOffsets(); + gotoLine(cs, ls); + } + mark = false; + mark(); + } else if (cut2end || toEnd) { + String l = lines.get(line); + int col = offsetInLine + column; + cutbuffer.add(l.substring(col)); + lines.set(line, l.substring(0, col)); + if (toEnd) { + line++; + while (true) { + cutbuffer.add(lines.get(line)); + lines.remove(line); + if (line > lines.size() - 1) { + line--; + break; + } + } + } + } else { + cutbuffer.add(lines.get(line)); + lines.remove(line); + offsetInLine = 0; + if (line > lines.size() - 1) { + line--; + } + } + display.clear(); + computeAllOffsets(); + dirty = true; + uncut = false; + } + } + + void uncut() { + if (cutbuffer.isEmpty()) { + return; + } + String l = lines.get(line); + int col = offsetInLine + column; + if (cut2end) { + lines.set(line, l.substring(0, col) + cutbuffer.get(0) + l.substring(col)); + computeAllOffsets(); + moveRight(col + cutbuffer.get(0).length(), true); + } else if (col == 0) { + lines.addAll(line, cutbuffer); + computeAllOffsets(); + if (cutbuffer.size() > 1) { + gotoLine(cutbuffer.get(cutbuffer.size() - 1).length(), line + cutbuffer.size()); + } else { + moveRight(cutbuffer.get(0).length(), true); + } + } else { + int gotol = line; + if (cutbuffer.size() == 1) { + lines.set(line, l.substring(0, col) + cutbuffer.get(0) + l.substring(col)); + } else { + lines.set(line++, l.substring(0, col) + cutbuffer.get(0)); + gotol = line; + lines.add(line, cutbuffer.get(cutbuffer.size() - 1) + l.substring(col)); + for (int i = cutbuffer.size() - 2; i > 0 ; i--) { + gotol++; + lines.add(line, cutbuffer.get(i)); + } + } + computeAllOffsets(); + if (cutbuffer.size() > 1) { + gotoLine(cutbuffer.get(cutbuffer.size() - 1).length(), gotol); + } else { + moveRight(col + cutbuffer.get(0).length(), true); + } + } + display.clear(); + dirty = true; + uncut = true; + } + + void mark() { + if (mark) { + markPos[0] = line; + markPos[1] = offsetInLine + column; + } else { + markPos[0] = -1; + markPos[1] = -1; + } + } + + int[] getMarkStart() { + int[] out = {-1, -1}; + if (!mark) { + return out; + } + if (markPos[0] > line || (markPos[0] == line && markPos[1] > offsetInLine + column) ) { + out[0] = line; + out[1] = offsetInLine + column; + } else { + out = markPos; + } + return out; + } + + int[] getMarkEnd() { + int[] out = {-1, -1}; + if (!mark) { + return out; + } + if (markPos[0] > line || (markPos[0] == line && markPos[1] > offsetInLine + column) ) { + out = markPos; + } else { + out[0] = line; + out[1] = offsetInLine + column; + } + return out; + } + } + + private static class SyntaxHighlighter { + private List rules = new ArrayList<>(); + private int ruleStartId = 0; + + public SyntaxHighlighter() {} + + public void addRule(AttributedStyle style, Pattern pattern) { + rules.add(new HighlightRule(style, pattern)); + } + + public void addRule(AttributedStyle style, Pattern start, Pattern end) { + rules.add(new HighlightRule(style, start, end)); + } + + public void reset() { + ruleStartId = 0; + } + + public AttributedString highlightNextLine(String line) { + return highlightNextLine(new AttributedString(line)); + } + + public AttributedString highlightNextLine(AttributedString line) { + if (rules.isEmpty()) { + return line; + } + AttributedStringBuilder asb = new AttributedStringBuilder(); + AttributedStyle as = new AttributedStyle(AttributedStyle.DEFAULT); + asb.append(line); + for (int i = ruleStartId; i < rules.size(); i++) { + HighlightRule rule = rules.get(i); + switch (rule.getType()) { + case PATTERN: + asb.styleMatches(rule.getPattern(), rule.getStyle()); + break; + case START_END: + boolean done = false; + Matcher start = rule.getStart().matcher(asb.toAttributedString()); + Matcher end = rule.getEnd().matcher(asb.toAttributedString()); + while (!done) { + AttributedStringBuilder a = new AttributedStringBuilder(); + if (ruleStartId == i) { // first rule should never be type + // START_END or we will fail here! + if (end.find()) { + a.append(asb.columnSubSequence(0, end.end()),rule.getStyle()); + a.append(asb.columnSubSequence(end.end(), asb.length())); + ruleStartId = 0; + } else { + a.append(asb, rule.getStyle()); + done = true; + } + asb = a; + } else { + if (start.find()) { + a.append(asb.columnSubSequence(0, start.start())); + if (end.find()) { + a.append(asb.columnSubSequence(start.start(), end.end()), rule.getStyle()); + a.append(asb.columnSubSequence(end.end(), asb.length())); + } else { + ruleStartId = i; + a.append(asb.columnSubSequence(start.start(),asb.length()), rule.getStyle()); + done = true; + } + asb = a; + } else { + done = true; + } + } + } + break; + } + } + return asb.toAttributedString(); + } + + private static class HighlightRule { + public enum RuleType {PATTERN, START_END}; + private RuleType type; + private Pattern pattern; + private AttributedStyle style; + private Pattern start; + private Pattern end; + + public HighlightRule(AttributedStyle style, Pattern pattern) { + this.type = RuleType.PATTERN; + this.pattern = pattern; + this.style = style; + } + + public HighlightRule(AttributedStyle style, Pattern start, Pattern end) { + this.type = RuleType.START_END; + this.style = style; + this.start = start; + this.end = end; + } + + public RuleType getType() { + return type; + } + + public AttributedStyle getStyle() { + return style; + } + + public Pattern getPattern() { + if (type == RuleType.START_END) { + throw new IllegalAccessError(); + } + return pattern; + } + + public Pattern getStart() { + if (type == RuleType.PATTERN) { + throw new IllegalAccessError(); + } + return start; + } + + public Pattern getEnd() { + if (type == RuleType.PATTERN) { + throw new IllegalAccessError(); + } + return end; + } + + } } public Nano(Terminal terminal, File root) { @@ -1141,6 +1609,31 @@ public void run() throws IOException { case TOGGLE_SUSPENSION: toggleSuspension(); break; + case COPY: + buffer.copy(); + break; + case CUT: + buffer.cut(); + break; + case UNCUT: + buffer.uncut(); + break; + case GOTO: + gotoLine(); + curPos(); + break; + case CUT_TO_END_TOGGLE: + cut2end = !cut2end; + setMessage("Cut to end " + (cut2end ? "enabled" : "disabled")); + break; + case CUT_TO_END: + buffer.cut(true); + break; + case MARK: + mark = !mark; + setMessage("Mark " + (mark ? "Set" : "Unset")); + buffer.mark(); + break; default: setMessage("Unsupported " + op.name().toLowerCase().replace('_', '-')); break; @@ -1162,6 +1655,30 @@ public void run() throws IOException { } } + private int editInputBuffer(Operation operation, int curPos) { + switch (operation) { + case INSERT: + editBuffer.insert(curPos++, bindingReader.getLastBinding()); + break; + case BACKSPACE: + if (curPos > 0) { + editBuffer.deleteCharAt(--curPos); + } + break; + case LEFT: + if (curPos > 0) { + curPos--; + } + break; + case RIGHT: + if (curPos < editBuffer.length()) { + curPos++; + } + break; + } + return curPos; + } + boolean write() throws IOException { KeyMap writeKeyMap = new KeyMap<>(); if (!restricted) { @@ -1187,7 +1704,7 @@ boolean write() throws IOException { writeKeyMap.bind(Operation.TOGGLE_SUSPENSION, alt('z')); writeKeyMap.bind(Operation.RIGHT, key(terminal, Capability.key_right)); writeKeyMap.bind(Operation.LEFT, key(terminal, Capability.key_left)); - + editMessage = getWriteMessage(); editBuffer.setLength(0); editBuffer.append(buffer.file == null ? "" : buffer.file); @@ -1195,25 +1712,8 @@ boolean write() throws IOException { this.shortcuts = writeShortcuts(); display(curPos); while (true) { - switch (readOperation(writeKeyMap)) { - case INSERT: - editBuffer.insert(curPos++, bindingReader.getLastBinding()); - break; - case BACKSPACE: - if (curPos > 0) { - editBuffer.deleteCharAt(--curPos); - } - break; - case LEFT: - if (curPos > 0) { - curPos--; - } - break; - case RIGHT: - if (curPos < editBuffer.length()) { - curPos++; - } - break; + Operation op = readOperation(writeKeyMap); + switch (op) { case CANCEL: editMessage = null; this.shortcuts = standardShortcuts(); @@ -1249,6 +1749,9 @@ boolean write() throws IOException { case TOGGLE_SUSPENSION: toggleSuspension(); break; + default: + curPos = editInputBuffer(op, curPos); + break; } editMessage = getWriteMessage(); display(curPos); @@ -1408,25 +1911,8 @@ void read() { this.shortcuts = readShortcuts(); display(curPos); while (true) { - switch (readOperation(readKeyMap)) { - case INSERT: - editBuffer.insert(curPos++, bindingReader.getLastBinding()); - break; - case BACKSPACE: - if (curPos > 0) { - editBuffer.deleteCharAt(--curPos); - } - break; - case LEFT: - if (curPos > 0) { - curPos--; - } - break; - case RIGHT: - if (curPos < editBuffer.length()) { - curPos++; - } - break; + Operation op = readOperation(readKeyMap); + switch (op) { case CANCEL: editMessage = null; this.shortcuts = standardShortcuts(); @@ -1468,8 +1954,8 @@ void read() { case MOUSE_EVENT: mouseEvent(); break; - case TOGGLE_SUSPENSION: - toggleSuspension(); + default: + curPos = editInputBuffer(op, curPos); break; } editMessage = getReadMessage(); @@ -1487,6 +1973,89 @@ private String getReadMessage() { return sb.toString(); } + void gotoLine() throws IOException { + KeyMap readKeyMap = new KeyMap<>(); + readKeyMap.setUnicode(Operation.INSERT); + for (char i = 32; i < 256; i++) { + readKeyMap.bind(Operation.INSERT, Character.toString(i)); + } + readKeyMap.bind(Operation.BACKSPACE, del()); + readKeyMap.bind(Operation.ACCEPT, "\r"); + readKeyMap.bind(Operation.HELP, ctrl('G'), key(terminal, Capability.key_f1)); + readKeyMap.bind(Operation.CANCEL, ctrl('C')); + readKeyMap.bind(Operation.RIGHT, key(terminal, Capability.key_right)); + readKeyMap.bind(Operation.LEFT, key(terminal, Capability.key_left)); + readKeyMap.bind(Operation.FIRST_LINE, ctrl('Y')); + readKeyMap.bind(Operation.LAST_LINE, ctrl('V')); + readKeyMap.bind(Operation.SEARCH, ctrl('T')); + + editMessage = "Enter line number, column number: "; + editBuffer.setLength(0); + int curPos = editBuffer.length(); + this.shortcuts = gotoShortcuts(); + display(curPos); + while (true) { + Operation op = readOperation(readKeyMap); + switch (op) { + case CANCEL: + editMessage = null; + this.shortcuts = standardShortcuts(); + return; + case FIRST_LINE: + editMessage = null; + buffer.firstLine(); + this.shortcuts = standardShortcuts(); + return; + case LAST_LINE: + editMessage = null; + buffer.lastLine(); + this.shortcuts = standardShortcuts(); + return; + case SEARCH: + search(); + return; + case ACCEPT: + editMessage = null; + String[] pos = editBuffer.toString().split(",", 2); + int[] args = { 0, 0 }; + try { + for(int i = 0; i < pos.length; i++) { + if (pos[i].trim().length() > 0) { + args[i] = Integer.parseInt(pos[i]) - 1; + if (args[i] < 0) { + throw new NumberFormatException(); + } + } + } + buffer.gotoLine(args[1], args[0]); + } catch (NumberFormatException ex) { + setMessage("Invalid line or column number"); + } catch (Exception ex) { + setMessage("Internal error: " + ex.getMessage()); + } + this.shortcuts = standardShortcuts(); + return; + case HELP: + help("nano-goto-help.txt"); + break; + default: + curPos = editInputBuffer(op, curPos); + break; + } + display(curPos); + } + } + + private LinkedHashMap gotoShortcuts() { + LinkedHashMap shortcuts = new LinkedHashMap<>(); + shortcuts.put("^G", "Get Help"); + shortcuts.put("^Y", "First Line"); + shortcuts.put("^T", "Go To Text"); + shortcuts.put("^C", "Cancel"); + shortcuts.put("^V", "Last Line"); + return shortcuts; + } + private LinkedHashMap readShortcuts() { LinkedHashMap shortcuts = new LinkedHashMap<>(); shortcuts.put("^G", "Get Help"); @@ -1657,25 +2226,8 @@ void search() throws IOException { display(curPos); try { while (true) { - switch (readOperation(searchKeyMap)) { - case INSERT: - editBuffer.insert(curPos++, bindingReader.getLastBinding()); - break; - case BACKSPACE: - if (curPos > 0) { - editBuffer.deleteCharAt(--curPos); - } - break; - case LEFT: - if (curPos > 0) { - curPos--; - } - break; - case RIGHT: - if (curPos < editBuffer.length()) { - curPos++; - } - break; + Operation op = readOperation(searchKeyMap); + switch (op) { case UP: searchTermId++; if (searchTermId >= 0 && searchTermId < searchTerms.size()) { @@ -1695,7 +2247,7 @@ void search() throws IOException { editBuffer.setLength(0); if (searchTermId < 0) { searchTermId = -1; - editBuffer.append(currentBuffer); + editBuffer.append(currentBuffer); } else { editBuffer.append(searchTerms.get(searchTermId)); } @@ -1742,7 +2294,10 @@ void search() throws IOException { case TOGGLE_SUSPENSION: toggleSuspension(); break; - } + default: + curPos = editInputBuffer(op, curPos); + break; + } editMessage = getSearchMessage(); display(curPos); } @@ -1908,6 +2463,7 @@ void oneMoreLine() { void wrap() { wrapping = !wrapping; + buffer.computeAllOffsets(); resetDisplay(); setMessage("Lines wrapping " + (wrapping ? "enabled" : "disabled")); } @@ -1951,7 +2507,7 @@ else if (event.getType() == MouseEvent.Type.Wheel) { } } } - + void toggleSuspension(){ if (restricted) { setMessage("This function is disabled in restricted mode"); @@ -1986,7 +2542,7 @@ void resetDisplay() { synchronized void display() { display(null); } - + synchronized void display(final Integer editCursor) { if (nbBindings > 0) { if (--nbBindings == 0) { @@ -2028,7 +2584,7 @@ protected List computeFooter() { } sb.append('\n'); footer.add(sb.toAttributedString()); - } else if (message != null || constantCursor) { + } else if (message!= null || constantCursor) { int rwidth = size.getColumns(); String text = "[ " + (message == null ? computeCurPos() : message) + " ]"; int len = text.length(); @@ -2119,7 +2675,7 @@ protected void bindKeys() { keys.bind(Operation.MARK, ctrl('^'), key(terminal, Capability.key_f15), alt('a')); keys.bind(Operation.NEXT_SEARCH, key(terminal, Capability.key_f16), alt('w')); - keys.bind(Operation.COPY, alt('^')); + keys.bind(Operation.COPY, alt('^'), alt('6')); keys.bind(Operation.INDENT, alt('}')); keys.bind(Operation.UNINDENT, alt('{')); @@ -2130,8 +2686,8 @@ protected void bindKeys() { keys.bind(Operation.UP, ctrl('P')); keys.bind(Operation.DOWN, ctrl('N')); - keys.bind(Operation.BEGINNING_OF_LINE, ctrl('A')); - keys.bind(Operation.END_OF_LINE, ctrl('E')); + keys.bind(Operation.BEGINNING_OF_LINE, ctrl('A'), key(terminal, Capability.key_home)); + keys.bind(Operation.END_OF_LINE, ctrl('E'), key(terminal, Capability.key_end)); keys.bind(Operation.BEGINNING_OF_PARAGRAPH, alt('('), alt('9')); keys.bind(Operation.END_OF_PARAGRAPH, alt(')'), alt('0')); keys.bind(Operation.FIRST_LINE, alt('\\'), alt('|')); @@ -2167,8 +2723,7 @@ protected void bindKeys() { keys.bind(Operation.SMART_HOME_KEY, alt('h')); keys.bind(Operation.AUTO_INDENT, alt('i')); keys.bind(Operation.CUT_TO_END_TOGGLE, alt('k')); - // TODO: reenable wrapping after fixing #120 - // keys.bind(Operation.WRAP, alt('l')); + keys.bind(Operation.WRAP, alt('l')); keys.bind(Operation.TABS_TO_SPACE, alt('q')); keys.bind(Operation.BACKUP, alt('b')); @@ -2180,9 +2735,7 @@ protected void bindKeys() { keys.bind(Operation.DOWN, key(terminal, Capability.key_down)); keys.bind(Operation.RIGHT, key(terminal, Capability.key_right)); keys.bind(Operation.LEFT, key(terminal, Capability.key_left)); - keys.bind(Operation.MOUSE_EVENT, key(terminal, Capability.key_mouse)); - keys.bind(Operation.TOGGLE_SUSPENSION, alt('z')); } @@ -2271,7 +2824,7 @@ protected enum Operation { UNCUT, MOUSE_EVENT, - + TOGGLE_SUSPENSION } diff --git a/builtins/src/main/resources/org/jline/builtins/less-help.txt b/builtins/src/main/resources/org/jline/builtins/less-help.txt index 9b9ba9de4..14dcdd03b 100644 --- a/builtins/src/main/resources/org/jline/builtins/less-help.txt +++ b/builtins/src/main/resources/org/jline/builtins/less-help.txt @@ -24,6 +24,8 @@ ESC-( LeftArrow * Right one half screen width (or N positions). g < ESC-< * Go to first line in file (or line N). G > ESC-> * Go to last line in file (or line N). + r ^R ^L Repaint screen. + R Repaint screen, discarding buffered input. --------------------------------------------------- Default "window" is the screen height. Default "half-window" is half of the screen height. @@ -43,6 +45,8 @@ CHANGING FILES + :e [file] Examine a new file. + ^X^V Same as :e. :n * Examine the (N-th) next file from the command line. :p * Examine the (N-th) previous file from the command line. :x * Examine the first (or N-th) file from the command line. diff --git a/builtins/src/main/resources/org/jline/builtins/nano-goto-help.txt b/builtins/src/main/resources/org/jline/builtins/nano-goto-help.txt new file mode 100644 index 000000000..ec5d4c7a3 --- /dev/null +++ b/builtins/src/main/resources/org/jline/builtins/nano-goto-help.txt @@ -0,0 +1,14 @@ +Go To Line Help Text + + Enter the line number that you wish to go to and hit Enter. If there are + fewer lines of text than the number you entered, you will be brought to + the last line of the file. + + The following function keys are available in Go To Line mode: + +^G (F1) Display this help text +^C Cancel the current function +^Y Go to the first line of the file +^V Go to the last line of the file + +^T Search for a string or a regular expression diff --git a/builtins/src/main/resources/org/jline/builtins/nano-main-help.txt b/builtins/src/main/resources/org/jline/builtins/nano-main-help.txt index 75e9dc2a1..4ad2fe419 100644 --- a/builtins/src/main/resources/org/jline/builtins/nano-main-help.txt +++ b/builtins/src/main/resources/org/jline/builtins/nano-main-help.txt @@ -29,15 +29,15 @@ M-^ (M-6) Copy the current line and store it in the cutbuffer M-} Indent the current line M-{ Unindent the current line -^F Move forward one character -^B Move back one character +^F (Right) Move forward one character +^B (Left) Move back one character ^Space Move forward one word M-Space Move back one word -^P Move to the previous line -^N Move to the next line +^P (Up) Move to the previous line +^N (Down) Move to the next line -^A Move to the beginning of the current line -^E Move to the end of the current line +^A (Home) Move to the beginning of the current line +^E (End) Move to the end of the current line M-( (M-9) Move to the beginning of the current paragraph M-) (M-0) Move to the end of the current paragraph M-\ (M-|) Move to the first line of the file @@ -77,5 +77,5 @@ M-Q Conversion of typed tabs to spaces enable/disable M-B Backup files enable/disable M-F Multiple file buffers enable/disable M-M Mouse support enable/disable -M-N No conversion from DOS/Mac format enable/disable +M-N Line numbers enable/disable M-Z Suspension enable/disable diff --git a/demo/src/main/java/org/apache/felix/gogo/jline/Posix.java b/demo/src/main/java/org/apache/felix/gogo/jline/Posix.java index cb6dd48dc..c52689e39 100644 --- a/demo/src/main/java/org/apache/felix/gogo/jline/Posix.java +++ b/demo/src/main/java/org/apache/felix/gogo/jline/Posix.java @@ -30,9 +30,12 @@ import java.io.OutputStream; import java.io.PrintStream; import java.io.Reader; +import java.net.MalformedURLException; +import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.LinkOption; import java.nio.file.Path; +import java.nio.file.PathMatcher; import java.nio.file.StandardWatchEventKinds; import java.nio.file.WatchKey; import java.nio.file.WatchService; @@ -82,6 +85,7 @@ import org.jline.builtins.Options.HelpException; import org.jline.builtins.Source; import org.jline.builtins.Source.PathSource; +import org.jline.builtins.Source.URLSource; import org.jline.builtins.TTop; import org.jline.terminal.Attributes; import org.jline.terminal.Terminal; @@ -964,6 +968,10 @@ protected void less(CommandSession session, Process process, String[] argv) thro for (String arg : opt.args()) { if ("-".equals(arg)) { sources.add(new StdInSource(process)); + } else if (arg.contains("*") || arg.contains("?")) { + PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:"+arg); + Files.find(session.currentDir(), Integer.MAX_VALUE, (path, f) -> pathMatcher.matches(path)) + .forEach(p -> sources.add(doUrlSource(session.currentDir(), p))); } else { sources.add(new PathSource(session.currentDir().resolve(arg), arg)); } @@ -984,7 +992,7 @@ protected void less(CommandSession session, Process process, String[] argv) thro tabs.add(parseInteger(s)); } } - Less less = new Less(Shell.getTerminal(session)).tabs(tabs); + Less less = new Less(Shell.getTerminal(session), session.currentDir()).tabs(tabs); less.quitAtFirstEof = opt.isSet("QUIT-AT-EOF"); less.quitAtSecondEof = opt.isSet("quit-at-eof"); less.quiet = opt.isSet("quiet"); @@ -1001,6 +1009,16 @@ protected void less(CommandSession session, Process process, String[] argv) thro less.run(sources); } + private static Source doUrlSource(Path currentDir, Path file) { + Source out = null; + try { + out = new URLSource(currentDir.resolve(file).toUri().toURL(), file.toString()); + } catch (MalformedURLException exp) { + throw new IllegalArgumentException(exp.getMessage()); + } + return out; + } + private int parseInteger(String s) throws IllegalArgumentException { try { return Integer.parseInt(s); diff --git a/reader/src/main/java/org/jline/reader/LineReader.java b/reader/src/main/java/org/jline/reader/LineReader.java index ec3e85f1d..5b3c6bf68 100644 --- a/reader/src/main/java/org/jline/reader/LineReader.java +++ b/reader/src/main/java/org/jline/reader/LineReader.java @@ -58,7 +58,7 @@ * Defaults to an empty string. * *
{@code %}n{@code P}c
- *
Insert padding at this possion, repeating the following + *
Insert padding at this position, repeating the following * character c as needed to bring the total prompt * column width as specified by the digits n. *