diff --git a/builtins/src/main/java/org/jline/builtins/Nano.java b/builtins/src/main/java/org/jline/builtins/Nano.java index b6484fdc9..447a7c309 100644 --- a/builtins/src/main/java/org/jline/builtins/Nano.java +++ b/builtins/src/main/java/org/jline/builtins/Nano.java @@ -404,7 +404,8 @@ void computeAllOffsets() { } } - LinkedList computeOffsets(String text) { + LinkedList computeOffsets(String line) { + String text = new AttributedStringBuilder().tabs(tabs).append(line).toString(); int width = size.getColumns() - (printLineNumbers ? 8 : 0); LinkedList offsets = new LinkedList<>(); offsets.add(0); @@ -661,7 +662,7 @@ private void cursorDown(int lines) { column = Math.min(wantedColumn, next - offsetInLine); } } - moveToChar(column); + moveToChar(offsetInLine + column); } private void cursorUp(int lines) { @@ -690,7 +691,7 @@ private void cursorUp(int lines) { } } } - moveToChar(column); + moveToChar(offsetInLine + column); } void ensureCursorVisible() { @@ -2898,8 +2899,8 @@ void searchAndReplace() { found = buffer.nextSearch(); if (found) { int[] re = buffer.highlightStart(); - int col = searchBackwards ? buffer.getLine(re[0]).length() - re[1] : re[1]; - int match = re[0]*100000 + col; + int col = searchBackwards ? buffer.length(buffer.getLine(re[0])) - re[1] : re[1]; + int match = re[0]*10000 + col; if (matches.contains(match)) { found = false; break; diff --git a/builtins/src/main/java/org/jline/builtins/Tmux.java b/builtins/src/main/java/org/jline/builtins/Tmux.java index ac7ec5f3c..464e9487d 100644 --- a/builtins/src/main/java/org/jline/builtins/Tmux.java +++ b/builtins/src/main/java/org/jline/builtins/Tmux.java @@ -39,6 +39,7 @@ import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; +import java.util.stream.IntStream; import org.jline.builtins.Options.HelpException; import org.jline.keymap.BindingReader; @@ -88,6 +89,14 @@ public class Tmux { public static final String CMD_BIND = "bind"; public static final String CMD_UNBIND_KEY = "unbind-key"; public static final String CMD_UNBIND = "unbind"; + public static final String CMD_NEW_WINDOW = "new-window"; + public static final String CMD_NEWW = "neww"; + public static final String CMD_NEXT_WINDOW = "next-window"; + public static final String CMD_NEXT = "next"; + public static final String CMD_PREVIOUS_WINDOW = "previous-window"; + public static final String CMD_PREV = "prev"; + public static final String CMD_LIST_WINDOWS = "list-windows"; + public static final String CMD_LSW = "lsw"; private static final int[][][] WINDOW_CLOCK_TABLE = { { { 1,1,1,1,1 }, /* 0 */ @@ -170,13 +179,11 @@ public class Tmux { private final PrintStream err; private final String term; private final Consumer runner; - private List panes = new CopyOnWriteArrayList<>(); - private VirtualConsole active; - private int lastActive; + private List windows = new ArrayList<>(); + private Integer windowsId = 0; + private int activeWindow = 0; private final AtomicBoolean running = new AtomicBoolean(true); private final Size size = new Size(); - private final AtomicInteger paneId = new AtomicInteger(); - private Layout layout; private boolean identify; private ScheduledExecutorService executor; @@ -190,6 +197,184 @@ enum Binding { Discard, SelfInsert, Mouse } + private class Window { + private List panes = new CopyOnWriteArrayList<>(); + private VirtualConsole active; + private int lastActive; + private final AtomicInteger paneId = new AtomicInteger(); + private Layout layout; + private Tmux tmux; + private String name; + + public Window(Tmux tmux) throws IOException { + this.tmux = tmux; + layout = new Layout(); + layout.sx = size.getColumns(); + layout.sy = size.getRows(); + layout.type = WindowPane; + active = new VirtualConsole(paneId.incrementAndGet(), term + , 0, 0, size.getColumns(), size.getRows() - 1 + , tmux::setDirty, tmux::close, layout); + active.active = lastActive++; + active.getConsole().setAttributes(terminal.getAttributes()); + panes.add(active); + name = "win" + (windowsId < 10 ? "0" + windowsId : windowsId); + windowsId++; + } + + public String getName() { + return name; + } + public List getPanes() { + return panes; + } + + public VirtualConsole getActive() { + return active; + } + + public void remove(VirtualConsole console) { + panes.remove(console); + if (!panes.isEmpty()) { + console.layout.remove(); + if (active == console) { + active = panes.stream() + .sorted(Comparator.comparingInt(p -> p.active).reversed()) + .findFirst().get(); + } + layout = active.layout; + while (layout.parent != null) { + layout = layout.parent; + } + layout.fixOffsets(); + layout.fixPanes(size.getColumns(), size.getRows()); + } + } + + public void handleResize() { + layout.resize(size.getColumns(), size.getRows() - 1); + panes.forEach(vc -> { + if (vc.width() != vc.layout.sx || vc.height() != vc.layout.sy + || vc.left() != vc.layout.xoff || vc.top() != vc.layout.yoff) { + vc.resize(vc.layout.xoff, vc.layout.yoff, vc.layout.sx, vc.layout.sy); + display.clear(); + } + }); + } + + public VirtualConsole splitPane(Options opt) throws IOException { + Layout.Type type = opt.isSet("horizontal") ? LeftRight : TopBottom; + // If we're splitting the main pane, create a parent + if (layout.type == WindowPane) { + Layout p = new Layout(); + p.sx = layout.sx; + p.sy = layout.sy; + p.type = type; + p.cells.add(layout); + layout.parent = p; + layout = p; + } + Layout cell = active.layout(); + if (opt.isSet("f")) { + while (cell.parent != layout) { + cell = cell.parent; + } + } + int size = -1; + if (opt.isSet("size")) { + size = opt.getNumber("size"); + } else if (opt.isSet("perc")) { + int p = opt.getNumber("perc"); + if (type == TopBottom) { + size = (cell.sy * p) / 100; + } else { + size = (cell.sx * p) / 100; + } + } + // Split now + Layout newCell = cell.split(type, size, opt.isSet("before")); + if (newCell == null) { + err.println("create pane failed: pane too small"); + return null; + } + + VirtualConsole newConsole = new VirtualConsole(paneId.incrementAndGet(), term + , newCell.xoff, newCell.yoff, newCell.sx, newCell.sy + , tmux::setDirty, tmux::close, newCell); + panes.add(newConsole); + newConsole.getConsole().setAttributes(terminal.getAttributes()); + if (!opt.isSet("d")) { + active = newConsole; + active.active = lastActive++; + } + return newConsole; + } + + public boolean selectPane(Options opt) { + VirtualConsole prevActive = active; + if (opt.isSet("L")) { + active = panes.stream() + .filter(c -> c.bottom() > active.top() && c.top() < active.bottom()) + .filter(c -> c != active) + .sorted(Comparator + .comparingInt(c -> c.left() > active.left() ? c.left() : c.left() + size.getColumns()).reversed() + .thenComparingInt(c -> - c.active)) + .findFirst().orElse(active); + } + else if (opt.isSet("R")) { + active = panes.stream() + .filter(c -> c.bottom() > active.top() && c.top() < active.bottom()) + .filter(c -> c != active) + .sorted(Comparator + .comparingInt(c -> c.left() > active.left() ? c.left() : c.left() + size.getColumns()) + .thenComparingInt(c -> - c.active)) + .findFirst().orElse(active); + } else if (opt.isSet("U")) { + active = panes.stream() + .filter(c -> c.right() > active.left() && c.left() < active.right()) + .filter(c -> c != active) + .sorted(Comparator + .comparingInt(c -> c.top() > active.top() ? c.top() : c.top() + size.getRows()).reversed() + .thenComparingInt(c -> - c.active)) + .findFirst().orElse(active); + } else if (opt.isSet("D")) { + active = panes.stream() + .filter(c -> c.right() > active.left() && c.left() < active.right()) + .filter(c -> c != active) + .sorted(Comparator + .comparingInt(c -> c.top() > active.top() ? c.top() : c.top() + size.getRows()) + .thenComparingInt(c -> - c.active)) + .findFirst().orElse(active); + } + boolean out = false; + if (prevActive != active) { + active.active = lastActive++; + out = true; + } + return out; + } + + public void resizePane(Options opt, int adjust) { + if (opt.isSet("width")) { + int x = opt.getNumber("width"); + active.layout().resizeTo(LeftRight, x); + } + if (opt.isSet("height")) { + int y = opt.getNumber("height"); + active.layout().resizeTo(TopBottom, y); + } + if (opt.isSet("L")) { + active.layout().resize(LeftRight, -adjust, true); + } else if (opt.isSet("R")) { + active.layout().resize(LeftRight, adjust, true); + } else if (opt.isSet("U")) { + active.layout().resize(TopBottom, -adjust, true); + } else if (opt.isSet("D")) { + active.layout().resize(TopBottom, adjust, true); + } + } + } + public Tmux(Terminal terminal, PrintStream err, Consumer runner) throws IOException { this.terminal = terminal; this.err = err; @@ -222,6 +407,9 @@ protected KeyMap createKeyMap(String prefix) { keyMap.bind(CMD_RESIZE_PANE + " -R", prefix + translate("^[[1;5D"), prefix + alt(translate("^[[D"))); // ctrl-right keyMap.bind(CMD_DISPLAY_PANES, prefix + "q"); keyMap.bind(CMD_CLOCK_MODE, prefix + "t"); + keyMap.bind(CMD_NEW_WINDOW, prefix + "c"); + keyMap.bind(CMD_NEXT_WINDOW, prefix + "n"); + keyMap.bind(CMD_PREVIOUS_WINDOW, prefix + "p"); return keyMap; } @@ -249,15 +437,9 @@ public void run() throws IOException { try { // Create first pane size.copy(terminal.getSize()); - layout = new Layout(); - layout.sx = size.getColumns(); - layout.sy = size.getRows(); - layout.type = WindowPane; - active = new VirtualConsole(paneId.incrementAndGet(), term, 0, 0, size.getColumns(), size.getRows() - 1, this::setDirty, this::close, layout); - active.active = lastActive++; - active.getConsole().setAttributes(terminal.getAttributes()); - panes.add(active); - runner.accept(active.getConsole()); + windows.add(new Window(this)); + activeWindow = 0; + runner.accept(active().getConsole()); // Start input loop new Thread(this::inputLoop, "Mux input loop").start(); // Redraw loop @@ -277,6 +459,18 @@ public void run() throws IOException { } } + private VirtualConsole active() { + return windows.get(activeWindow).getActive(); + } + + private List panes() { + return windows.get(activeWindow).getPanes(); + } + + private Window window() { + return windows.get(activeWindow); + } + private void redrawLoop() { while (running.get()) { try { @@ -314,22 +508,22 @@ private void inputLoop() { b = null; } if (b == Binding.SelfInsert) { - if (active.clock) { - active.clock = false; - if (clockFuture != null && panes.stream().noneMatch(vc -> vc.clock)) { + if (active().clock) { + active().clock = false; + if (clockFuture != null && panes().stream().noneMatch(vc -> vc.clock)) { clockFuture.cancel(false); clockFuture = null; } setDirty(); } else { - active.getMasterInputOutput().write(reader.getLastBinding().getBytes()); + active().getMasterInputOutput().write(reader.getLastBinding().getBytes()); first = false; } } else { if (first) { first = false; } else { - active.getMasterInputOutput().flush(); + active().getMasterInputOutput().flush(); first = true; } if (b == Binding.Mouse) { @@ -362,25 +556,29 @@ private void inputLoop() { } private synchronized void close(VirtualConsole terminal) { - int idx = panes.indexOf(terminal); + int idx = -1; + Window window = null; + for (Window w: windows) { + idx = w.getPanes().indexOf(terminal); + if (idx >= 0) { + window = w; + break; + } + } if (idx >= 0) { - panes.remove(idx); - if (panes.isEmpty()) { - running.set(false); - setDirty(); - } else { - terminal.layout.remove(); - if (active == terminal) { - active = panes.stream() - .sorted(Comparator.comparingInt(p -> p.active).reversed()) - .findFirst().get(); - } - layout = active.layout; - while (layout.parent != null) { - layout = layout.parent; + window.remove(terminal); + if (window.getPanes().isEmpty()) { + if (windows.size() > 1) { + windows.remove(window); + if (activeWindow >= windows.size()) { + activeWindow--; + } + resize(Signal.WINCH); + } else { + running.set(false); + setDirty(); } - layout.fixOffsets(); - layout.fixPanes(size.getColumns(), size.getRows()); + } else { resize(Signal.WINCH); } } @@ -392,11 +590,11 @@ private void resize(Signal signal) { } private void interrupt(Signal signal) { - active.getConsole().raise(signal); + active().getConsole().raise(signal); } private void suspend(Signal signal) { - active.getConsole().raise(signal); + active().getConsole().raise(signal); } private void handleResize() { @@ -404,14 +602,7 @@ private void handleResize() { if (resized.compareAndSet(true, false)) { size.copy(terminal.getSize()); } - layout.resize(size.getColumns(), size.getRows() - 1); - panes.forEach(vc -> { - if (vc.width() != vc.layout.sx || vc.height() != vc.layout.sy - || vc.left() != vc.layout.xoff || vc.top() != vc.layout.yoff) { - vc.resize(vc.layout.xoff, vc.layout.yoff, vc.layout.sx, vc.layout.sy); - display.clear(); - } - }); + window().handleResize(); } public void execute(PrintStream out, PrintStream err, String command) throws Exception { @@ -465,7 +656,106 @@ public synchronized void execute(PrintStream out, PrintStream err, List case CMD_SET: setOption(out, err, args); break; + case CMD_NEW_WINDOW: + case CMD_NEWW: + newWindow(out, err, args); + break; + case CMD_NEXT_WINDOW: + case CMD_NEXT: + nextWindow(out, err, args); + break; + case CMD_PREVIOUS_WINDOW: + case CMD_PREV: + previousWindow(out, err, args); + break; + case CMD_LIST_WINDOWS: + case CMD_LSW: + listWindows(out, err, args); + break; + } + } + + protected void listWindows(PrintStream out, PrintStream err, List args) throws Exception { + final String[] usage = { + "list-windows - ", + "Usage: list-windows", + " -? --help Show help" + }; + Options opt = Options.compile(usage).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); } + IntStream.range(0, windows.size()) + .mapToObj(i -> { + StringBuilder sb = new StringBuilder(); + sb.append(i); + sb.append(": "); + sb.append(windows.get(i).getName()); + sb.append(i == activeWindow ? "* " : " "); + sb.append("("); + sb.append(windows.get(i).getPanes().size()); + sb.append(" panes)"); + if (i == activeWindow) { + sb.append(" (active)"); + } + return sb.toString(); + }) + .sorted() + .forEach(out::println); + } + + protected void previousWindow(PrintStream out, PrintStream err, List args) throws Exception { + final String[] usage = { + "previous-window - ", + "Usage: previous-window", + " -? --help Show help" + }; + Options opt = Options.compile(usage).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + if (windows.size() > 1) { + activeWindow--; + if (activeWindow < 0) { + activeWindow = windows.size() - 1; + } + setDirty(); + } + } + + protected void nextWindow(PrintStream out, PrintStream err, List args) throws Exception { + final String[] usage = { + "next-window - ", + "Usage: next-window", + " -? --help Show help" + }; + Options opt = Options.compile(usage).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + if (windows.size() > 1) { + activeWindow++; + if (activeWindow >= windows.size()) { + activeWindow = 0; + } + setDirty(); + } + } + + protected void newWindow(PrintStream out, PrintStream err, List args) throws Exception { + final String[] usage = { + "new-window - ", + "Usage: new-window", + " -? --help Show help" + }; + Options opt = Options.compile(usage).parse(args); + if (opt.isSet("help")) { + throw new HelpException(opt.usage()); + } + windows.add(new Window(this)); + activeWindow = windows.size() - 1; + runner.accept(active().getConsole()); + setDirty(); } protected void setOption(PrintStream out, PrintStream err, List args) throws Exception { @@ -604,7 +894,7 @@ protected void sendKeys(PrintStream out, PrintStream err, List args) thr for (int i = 0, n = opt.getNumber("number"); i < n; i++) { for (String arg : opt.args()) { String s = opt.isSet("literal") ? arg : KeyMap.translate(arg); - active.getMasterInputOutput().write(s.getBytes()); + active().getMasterInputOutput().write(s.getBytes()); } } } @@ -619,7 +909,7 @@ protected void clockMode(PrintStream out, PrintStream err, List args) th if (opt.isSet("help")) { throw new HelpException(opt.usage()); } - active.clock = true; + active().clock = true; if (clockFuture == null) { long initial = Instant.now().until(Instant.now().truncatedTo(ChronoUnit.MINUTES).plusSeconds(60), ChronoUnit.MILLIS); @@ -671,23 +961,7 @@ protected void resizePane(PrintStream out, PrintStream err, List args) t } else { throw new HelpException(opt.usage()); } - if (opt.isSet("width")) { - int x = opt.getNumber("width"); - active.layout().resizeTo(LeftRight, x); - } - if (opt.isSet("height")) { - int y = opt.getNumber("height"); - active.layout().resizeTo(TopBottom, y); - } - if (opt.isSet("L")) { - active.layout().resize(LeftRight, -adjust, true); - } else if (opt.isSet("R")) { - active.layout().resize(LeftRight, adjust, true); - } else if (opt.isSet("U")) { - active.layout().resize(TopBottom, -adjust, true); - } else if (opt.isSet("D")) { - active.layout().resize(TopBottom, adjust, true); - } + window().resizePane(opt, adjust); setDirty(); } @@ -705,44 +979,8 @@ protected void selectPane(PrintStream out, PrintStream err, List args) t if (opt.isSet("help")) { throw new HelpException(opt.usage()); } - VirtualConsole prevActive = active; - if (opt.isSet("L")) { - active = panes.stream() - .filter(c -> c.bottom() > active.top() && c.top() < active.bottom()) - .filter(c -> c != active) - .sorted(Comparator - .comparingInt(c -> c.left() > active.left() ? c.left() : c.left() + size.getColumns()).reversed() - .thenComparingInt(c -> - c.active)) - .findFirst().orElse(active); - } - else if (opt.isSet("R")) { - active = panes.stream() - .filter(c -> c.bottom() > active.top() && c.top() < active.bottom()) - .filter(c -> c != active) - .sorted(Comparator - .comparingInt(c -> c.left() > active.left() ? c.left() : c.left() + size.getColumns()) - .thenComparingInt(c -> - c.active)) - .findFirst().orElse(active); - } else if (opt.isSet("U")) { - active = panes.stream() - .filter(c -> c.right() > active.left() && c.left() < active.right()) - .filter(c -> c != active) - .sorted(Comparator - .comparingInt(c -> c.top() > active.top() ? c.top() : c.top() + size.getRows()).reversed() - .thenComparingInt(c -> - c.active)) - .findFirst().orElse(active); - } else if (opt.isSet("D")) { - active = panes.stream() - .filter(c -> c.right() > active.left() && c.left() < active.right()) - .filter(c -> c != active) - .sorted(Comparator - .comparingInt(c -> c.top() > active.top() ? c.top() : c.top() + size.getRows()) - .thenComparingInt(c -> - c.active)) - .findFirst().orElse(active); - } - if (prevActive != active) { + if (window().selectPane(opt)) { setDirty(); - active.active = lastActive++; } } @@ -756,7 +994,7 @@ protected void sendPrefix(PrintStream out, PrintStream err, List args) t if (opt.isSet("help")) { throw new HelpException(opt.usage()); } - active.getMasterInputOutput().write(serverOptions.get(OPT_PREFIX).getBytes()); + active().getMasterInputOutput().write(serverOptions.get(OPT_PREFIX).getBytes()); } protected void splitWindow(PrintStream out, PrintStream err, List args) throws Exception { @@ -776,48 +1014,7 @@ protected void splitWindow(PrintStream out, PrintStream err, List args) if (opt.isSet("help")) { throw new HelpException(opt.usage()); } - Layout.Type type = opt.isSet("horizontal") ? LeftRight : TopBottom; - // If we're splitting the main pane, create a parent - if (layout.type == WindowPane) { - Layout p = new Layout(); - p.sx = layout.sx; - p.sy = layout.sy; - p.type = type; - p.cells.add(layout); - layout.parent = p; - layout = p; - } - Layout cell = active.layout(); - if (opt.isSet("f")) { - while (cell.parent != layout) { - cell = cell.parent; - } - } - int size = -1; - if (opt.isSet("size")) { - size = opt.getNumber("size"); - } else if (opt.isSet("perc")) { - int p = opt.getNumber("perc"); - if (type == TopBottom) { - size = (cell.sy * p) / 100; - } else { - size = (cell.sx * p) / 100; - } - } - // Split now - Layout newCell = cell.split(type, size, opt.isSet("before")); - if (newCell == null) { - err.println("create pane failed: pane too small"); - return; - } - - VirtualConsole newConsole = new VirtualConsole(paneId.incrementAndGet(), term, newCell.xoff, newCell.yoff, newCell.sx, newCell.sy, this::setDirty, this::close, newCell); - panes.add(newConsole); - newConsole.getConsole().setAttributes(terminal.getAttributes()); - if (!opt.isSet("d")) { - active = newConsole; - active.active = lastActive++; - } + VirtualConsole newConsole = window().splitPane(opt); runner.accept(newConsole.getConsole()); setDirty(); } @@ -835,37 +1032,37 @@ protected synchronized void redraw() { // Fill Arrays.fill(screen, 0x00000020L); int[] cursor = new int[2]; - for (VirtualConsole terminal : panes) { + for (VirtualConsole terminal : panes()) { if (terminal.clock) { String str = DateFormat.getTimeInstance(DateFormat.SHORT).format(new Date()); print(screen, terminal, str, CLOCK_COLOR); } else { // Dump terminal terminal.dump(screen, terminal.top(), terminal.left(), size.getRows(), size.getColumns(), - terminal == active ? cursor : null); + terminal == active() ? cursor : null); } if (identify) { String id = Integer.toString(terminal.id); - print(screen, terminal, id, terminal == active ? ACTIVE_COLOR : INACTIVE_COLOR); + print(screen, terminal, id, terminal == active() ? ACTIVE_COLOR : INACTIVE_COLOR); } // Draw border drawBorder(screen, size, terminal, 0x0L); } - drawBorder(screen, size, active, 0x010080000L << 32); + drawBorder(screen, size, active(), 0x010080000L << 32); // Draw status Arrays.fill(screen, (size.getRows() - 1) * size.getColumns(), size.getRows() * size.getColumns(), 0x20000080L << 32 | 0x0020L); // Attribute mask: 0xYXFFFBBB00000000L - // X: Bit 0 - Underlined - // Bit 1 - Negative - // Bit 2 - Concealed + // X: Bit 0 - Underlined + // Bit 1 - Negative + // Bit 2 - Concealed // Bit 3 - Bold // Y: Bit 0 - Foreground set // Bit 1 - Background set - // F: Foreground r-g-b - // B: Background r-g-b + // F: Foreground r-g-b + // B: Background r-g-b List lines = new ArrayList<>(); int prevBg = 0; diff --git a/builtins/src/main/java/org/jline/builtins/Widgets.java b/builtins/src/main/java/org/jline/builtins/Widgets.java new file mode 100644 index 000000000..46cc9d52e --- /dev/null +++ b/builtins/src/main/java/org/jline/builtins/Widgets.java @@ -0,0 +1,760 @@ +/* + * Copyright (c) 2002-2019, the original author or authors. + * + * This software is distributable under the BSD license. See the terms of the + * BSD license in the documentation provided with this software. + * + * https://opensource.org/licenses/BSD-3-Clause + */ +package org.jline.builtins; + +import static org.jline.keymap.KeyMap.del; +import static org.jline.keymap.KeyMap.ctrl; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jline.keymap.KeyMap; +import org.jline.reader.Binding; +import org.jline.reader.Buffer; +import org.jline.reader.LineReader; +import org.jline.reader.LineReader.SuggestionType; +import org.jline.reader.Reference; +import org.jline.reader.Widget; +import org.jline.reader.impl.BufferImpl; +import org.jline.utils.AttributedString; +import org.jline.utils.AttributedStringBuilder; +import org.jline.utils.AttributedStyle; +import org.jline.utils.Status; + +public abstract class Widgets { + private final LineReader reader; + + public Widgets(LineReader reader) { + this.reader = reader; + } + + public void addWidget(String name, Widget widget) { + reader.getWidgets().put(name, namedWidget(name, widget)); + } + + private Widget namedWidget(final String name, final Widget widget) { + return new Widget() { + @Override + public String toString() { + return name; + } + @Override + public boolean apply() { + return widget.apply(); + } + }; + } + + public void callWidget(String name) { + reader.callWidget(name); + } + + public KeyMap getKeyMap(String name) { + return reader.getKeyMaps().get(name); + } + + public Buffer buffer() { + return reader.getBuffer(); + } + + public void replaceBuffer(Buffer buffer) { + reader.getBuffer().copyFrom(buffer); + } + + public List args(String line) { + return reader.getParser().parse(line, 0).words(); + } + + public String prevChar() { + return String.valueOf((char)reader.getBuffer().prevChar()); + } + + public String currChar() { + return String.valueOf((char)reader.getBuffer().currChar()); + } + + public String lastBinding() { + return reader.getLastBinding(); + } + + public void putString(String string) { + reader.getBuffer().write(string); + } + + public String tailTip() { + return reader.getTailTip(); + } + + public void setTailTip(String tailTip) { + reader.setTailTip(tailTip); + } + + public void clearTailTip() { + reader.setTailTip(""); + } + + public void setSuggestionType(SuggestionType type) { + reader.setAutosuggestion(type); + } + + public void addDescription(List desc) { + Status.getStatus(reader.getTerminal()).update(desc); + } + + public void clearDescription() { + initDescription(0); + } + + public void initDescription(int size) { + Status status = Status.getStatus(reader.getTerminal(), false); + if (size > 0) { + if (status == null) { + status = Status.getStatus(reader.getTerminal()); + } + status.setBorder(true); + List as = new ArrayList<>(); + for (int i = 0; i < size; i++) { + as.add(new AttributedString("")); + } + addDescription(as); + } else if (status != null) { + status.clear(); + } + } + + + public static class AutopairWidgets extends Widgets { + /* + * Inspired by zsh-autopair + * https://github.com/hlissner/zsh-autopair + */ + private static final Map LBOUNDS; + private static final Map RBOUNDS; + private final Map pairs; + private final Map defaultBindings = new HashMap<>(); + private boolean autopair = false; + { + pairs = new HashMap<>(); + pairs.put("`", "`"); + pairs.put("'", "'"); + pairs.put("\"", "\""); + pairs.put("[", "]"); + pairs.put("(", ")"); + pairs.put(" ", " "); + } + static { + LBOUNDS = new HashMap<>(); + LBOUNDS.put("all", "[.:/\\!]"); + LBOUNDS.put("quotes", "[\\]})a-zA-Z0-9]"); + LBOUNDS.put("spaces", "[^{(\\[]"); + LBOUNDS.put("braces", ""); + LBOUNDS.put("`", "`"); + LBOUNDS.put("\"", "\""); + LBOUNDS.put("'", "'"); + RBOUNDS = new HashMap<>(); + RBOUNDS.put("all", "[\\[{(<,.:?/%$!a-zA-Z0-9]"); + RBOUNDS.put("quotes", "[a-zA-Z0-9]"); + RBOUNDS.put("spaces", "[^\\]})]"); + RBOUNDS.put("braces", ""); + RBOUNDS.put("`", ""); + RBOUNDS.put("\"", ""); + RBOUNDS.put("'", ""); + } + + public AutopairWidgets(LineReader reader) { + this(reader, false); + } + + public AutopairWidgets(LineReader reader, boolean addCurlyBrackets) { + super(reader); + if (addCurlyBrackets) { + pairs.put("{", "}"); + } + addWidget("_autopair-insert", this::autopairInsert); + addWidget("_autopair-close", this::autopairClose); + addWidget("_autopair-delete", this::autopairDelete); + addWidget("autopair-toggle", this::toggleKeyBindings); + + KeyMap map = getKeyMap(LineReader.MAIN); + for (Map.Entry p: pairs.entrySet()) { + defaultBindings.put(p.getKey(), map.getBound(p.getKey())); + if (!p.getKey().equals(p.getValue())) { + defaultBindings.put(p.getValue(), map.getBound(p.getValue())); + } + } + defaultBindings.put(ctrl('H'), map.getBound(ctrl('H'))); + defaultBindings.put(del(), map.getBound(del())); + } + + /* + * Widgets + */ + public boolean autopairInsert() { + if (pairs.containsKey(lastBinding())) { + if (canSkip(lastBinding())) { + callWidget(LineReader.FORWARD_CHAR); + } else if (canPair(lastBinding())) { + callWidget(LineReader.SELF_INSERT); + putString(pairs.get(lastBinding())); + callWidget(LineReader.BACKWARD_CHAR); + } else { + callWidget(LineReader.SELF_INSERT); + } + } else { + callWidget(LineReader.SELF_INSERT); + } + return true; + } + + public boolean autopairClose() { + if (pairs.containsValue(lastBinding()) + && currChar().equals(lastBinding())) { + callWidget(LineReader.FORWARD_CHAR); + } else { + callWidget(LineReader.SELF_INSERT); + } + return true; + } + + public boolean autopairDelete() { + if (pairs.containsKey(prevChar()) && pairs.get(prevChar()).equals(currChar()) + && canDelete(prevChar())) { + callWidget(LineReader.DELETE_CHAR); + } + callWidget(LineReader.BACKWARD_DELETE_CHAR); + return true; + } + + public boolean toggleKeyBindings() { + if (autopair) { + defaultBindings(); + } else { + autopairBindings(); + } + return autopair; + } + /* + * key bindings... + * + */ + private void autopairBindings() { + KeyMap map = getKeyMap(LineReader.MAIN); + for (Map.Entry p: pairs.entrySet()) { + map.bind(new Reference("_autopair-insert"), p.getKey()); + if (!p.getKey().equals(p.getValue())) { + map.bind(new Reference("_autopair-close"), p.getValue()); + } + } + map.bind(new Reference("_autopair-delete"), ctrl('H')); + map.bind(new Reference("_autopair-delete"), del()); + autopair = true; + } + + private void defaultBindings() { + KeyMap map = getKeyMap(LineReader.MAIN); + for (Map.Entry p: pairs.entrySet()) { + map.bind(defaultBindings.get(p.getKey()), p.getKey()); + if (!p.getKey().equals(p.getValue())) { + map.bind(defaultBindings.get(p.getValue()), p.getValue()); + } + } + map.bind(defaultBindings.get(ctrl('H')), ctrl('H')); + map.bind(defaultBindings.get(del()), del()); + autopair = false; + } + /* + * helpers + */ + private boolean canPair(String d) { + if (balanced(d) && !nexToBoundary(d)) { + if (d.equals(" ") && (prevChar().equals(" ") || currChar().equals(" "))) { + return false; + } else { + return true; + } + } + return false; + } + + private boolean canSkip(String d) { + if (pairs.get(d).equals(d) && d.charAt(0) != ' ' && currChar().equals(d) + && balanced(d)) { + return true; + } + return false; + } + + private boolean canDelete(String d) { + if (balanced(d)) { + return true; + } + return false; + } + + private boolean balanced(String d) { + boolean out = false; + Buffer buf = buffer(); + String lbuf = buf.upToCursor(); + String rbuf = buf.substring(lbuf.length()); + String regx1 = pairs.get(d).equals(d)? d : "\\"+d; + String regx2 = pairs.get(d).equals(d)? pairs.get(d) : "\\"+pairs.get(d); + int llen = lbuf.length() - lbuf.replaceAll(regx1, "").length(); + int rlen = rbuf.length() - rbuf.replaceAll(regx2, "").length(); + if (llen == 0 && rlen == 0) { + out = true; + } else if (d.charAt(0) == ' ') { + out = true; + } else if (pairs.get(d).equals(d)) { + if ( llen == rlen || (llen + rlen) % 2 == 0 ) { + out = true; + } + } else { + int l2len = lbuf.length() - lbuf.replaceAll(regx2, "").length(); + int r2len = rbuf.length() - rbuf.replaceAll(regx1, "").length(); + int ltotal = llen - l2len; + int rtotal = rlen - r2len; + if (ltotal < 0) { + ltotal = 0; + } + if (ltotal >= rtotal) { + out = true; + } + } + return out; + } + + private boolean boundary(String lb, String rb) { + if ((lb.length() > 0 && prevChar().matches(lb)) + || + (rb.length() > 0 && currChar().matches(rb))) { + return true; + } + return false; + } + + private boolean nexToBoundary(String d) { + List bk = new ArrayList<>(); + bk.add("all"); + if (d.matches("['\"`]")) { + bk.add("quotes"); + } else if (d.matches("[{\\[(<]")) { + bk.add("braces"); + } else if (d.charAt(0) == ' ') { + bk.add("spaces"); + } + if (LBOUNDS.containsKey(d) && RBOUNDS.containsKey(d)) { + bk.add(d); + } + for (String k: bk) { + if (boundary(LBOUNDS.get(k), RBOUNDS.get(k))) { + return true; + } + } + return false; + } + } + + public static class AutosuggestionWidgets extends Widgets { + private final Map> defaultBindings = new HashMap<>(); + private boolean autosuggestion = false; + + public AutosuggestionWidgets(LineReader reader) { + super(reader); + addWidget("_autosuggest-forward-char", this::autosuggestForwardChar); + addWidget("_autosuggest-end-of-line", this::autosuggestEndOfLine); + addWidget("_autosuggest-forward-word", this::partialAccept); + addWidget("autosuggest-toggle", this::toggleKeyBindings); + KeyMap map = getKeyMap(LineReader.MAIN); + for (Map.Entry bound : map.getBoundKeys().entrySet()) { + if (bound.getValue() instanceof Reference) { + Reference w = (Reference)bound.getValue(); + if (w.name().equals(LineReader.FORWARD_CHAR)){ + addKeySequence(w, bound.getKey()); + } else if (w.name().equals(LineReader.END_OF_LINE)){ + addKeySequence(w, bound.getKey()); + } else if (w.name().equals(LineReader.FORWARD_WORD)){ + addKeySequence(w, bound.getKey()); + } + } + } + } + + private void addKeySequence(Reference widget, String keySequence) { + if (!defaultBindings.containsKey(widget)) { + defaultBindings.put(widget, new HashSet()); + } + defaultBindings.get(widget).add(keySequence); + } + /* + * Widgets + */ + public boolean partialAccept() { + Buffer buffer = buffer(); + if (buffer.cursor() == buffer.length()) { + int curPos = buffer.cursor(); + buffer.write(tailTip()); + buffer.cursor(curPos); + replaceBuffer(buffer); + callWidget(LineReader.FORWARD_WORD); + Buffer newBuf = new BufferImpl(); + newBuf.write(buffer().substring(0, buffer().cursor())); + replaceBuffer(newBuf); + } else { + callWidget(LineReader.FORWARD_WORD); + } + return true; + } + + public boolean autosuggestForwardChar() { + return accept(LineReader.FORWARD_CHAR); + } + + public boolean autosuggestEndOfLine() { + return accept(LineReader.END_OF_LINE); + } + + public boolean toggleKeyBindings() { + if (autosuggestion) { + defaultBindings(); + } else { + autosuggestionBindings(); + } + return autosuggestion; + } + + + private boolean accept(String widget) { + Buffer buffer = buffer(); + if (buffer.cursor() == buffer.length()) { + putString(tailTip()); + } else { + callWidget(widget); + } + return true; + } + /* + * key bindings... + * + */ + public void autosuggestionBindings() { + if (autosuggestion) { + return; + } + KeyMap map = getKeyMap(LineReader.MAIN); + for (Map.Entry> entry : defaultBindings.entrySet()) { + if (entry.getKey().name().equals(LineReader.FORWARD_CHAR)) { + for (String s: entry.getValue()) { + map.bind(new Reference("_autosuggest-forward-char"), s); + } + } else if (entry.getKey().name().equals(LineReader.END_OF_LINE)) { + for (String s: entry.getValue()) { + map.bind(new Reference("_autosuggest-end-of-line"), s); + } + } else if (entry.getKey().name().equals(LineReader.FORWARD_WORD)) { + for (String s: entry.getValue()) { + map.bind(new Reference("_autosuggest-forward-word"), s); + } + } + } + autosuggestion = true; + setSuggestionType(SuggestionType.HISTORY); + } + + public void defaultBindings() { + if (!autosuggestion) { + return; + } + KeyMap map = getKeyMap(LineReader.MAIN); + for (Map.Entry> entry : defaultBindings.entrySet()) { + for (String s: entry.getValue()) { + map.bind(entry.getKey(), s); + } + } + autosuggestion = false; + setSuggestionType(SuggestionType.NONE); + } + } + + public static class TailTipWidgets extends Widgets { + public enum TipType { + TAIL_TIP, + COMPLETER, + COMBINED + } + private final Map> defaultBindings = new HashMap<>(); + private boolean autosuggestion = false; + private Map> tailTips = new HashMap<>(); + private TipType tipType; + private int descriptionSize = 0; + + public TailTipWidgets(LineReader reader, Map> tailTips) { + this(reader, tailTips, 0, TipType.COMBINED); + } + + public TailTipWidgets(LineReader reader, Map> tailTips, TipType tipType) { + this(reader, tailTips, 0, tipType); + } + + public TailTipWidgets(LineReader reader, Map> tailTips, int descriptionSize) { + this(reader, tailTips, descriptionSize, TipType.COMBINED); + } + + public TailTipWidgets(LineReader reader, Map> tailTips, int descriptionSize, TipType tipType) { + super(reader); + this.tailTips = new HashMap<>(tailTips); + this.descriptionSize = descriptionSize; + this.tipType = tipType; + initDescription(descriptionSize); + addWidget("_tailtip-accept-line", this::tailtipAcceptLine); + addWidget("_tailtip-insert", this::tailtipInsert); + addWidget("_tailtip-backward-delete-char", this::tailtipBackwardDelete); + addWidget("_tailtip-delete-char", this::tailtipDelete); + addWidget("_tailtip-expand-or-complete", this::tailtipComplete); + addWidget("tailtip-toggle", this::toggleKeyBindings); + KeyMap map = getKeyMap(LineReader.MAIN); + for (Map.Entry bound : map.getBoundKeys().entrySet()) { + if (bound.getValue() instanceof Reference) { + Reference w = (Reference)bound.getValue(); + if (w.name().equals(LineReader.ACCEPT_LINE)){ + addKeySequence(w, bound.getKey()); + } else if (w.name().equals(LineReader.BACKWARD_DELETE_CHAR)){ + addKeySequence(w, bound.getKey()); + } else if (w.name().equals(LineReader.DELETE_CHAR)){ + addKeySequence(w, bound.getKey()); + } else if (w.name().equals(LineReader.EXPAND_OR_COMPLETE)){ + addKeySequence(w, bound.getKey()); + } + } + } + } + + private void addKeySequence(Reference widget, String keySequence) { + if (!defaultBindings.containsKey(widget)) { + defaultBindings.put(widget, new HashSet()); + } + defaultBindings.get(widget).add(keySequence); + } + + public void setDescriptionSize(int descriptionSize) { + this.descriptionSize = descriptionSize; + initDescription(descriptionSize); + } + + public int getDescriptionSize() { + return descriptionSize; + } + + public void setTipType(TipType type) { + this.tipType = type; + if (tipType == TipType.TAIL_TIP) { + setSuggestionType(SuggestionType.TAIL_TIP); + } else { + setSuggestionType(SuggestionType.COMPLETER); + } + } + + public TipType getTipType() { + return tipType; + } + + public boolean isActive() { + return autosuggestion; + } + + /* + * widgets + */ + public boolean tailtipComplete() { + return doTailTip(LineReader.EXPAND_OR_COMPLETE); + } + + public boolean tailtipAcceptLine() { + if (tipType != TipType.TAIL_TIP){ + setSuggestionType(SuggestionType.COMPLETER); + } + clearDescription(); + return clearTailTip(LineReader.ACCEPT_LINE); + } + + public boolean tailtipBackwardDelete() { + return doTailTip(LineReader.BACKWARD_DELETE_CHAR); + } + + private boolean clearTailTip(String widget) { + clearTailTip(); + callWidget(widget); + return true; + } + + public boolean tailtipDelete() { + clearTailTip(); + return doTailTip(LineReader.DELETE_CHAR); + } + + public boolean tailtipInsert() { + return doTailTip(LineReader.SELF_INSERT); + } + + private boolean doTailTip(String widget) { + Buffer buffer = buffer(); + callWidget(widget); + if (buffer.length() == buffer.cursor() + && ((!widget.equals(LineReader.BACKWARD_DELETE_CHAR) && prevChar().equals(" ")) || + (widget.equals(LineReader.BACKWARD_DELETE_CHAR) && !prevChar().equals(" ")))) { + List bp = args(buffer.toString()); + int bpsize = bp.size() + (widget.equals(LineReader.BACKWARD_DELETE_CHAR) ? -1 : 0); + List desc = new ArrayList<>(); + if (bpsize > 0 && tailTips.containsKey(bp.get(0))) { + List params = tailTips.get(bp.get(0)); + setSuggestionType(tipType == TipType.COMPLETER ? SuggestionType.COMPLETER : SuggestionType.TAIL_TIP); + if (bpsize - 1 < params.size()) { + desc = params.get(bpsize - 1).getDescription(); + StringBuilder tip = new StringBuilder(); + for (int i = bpsize - 1; i < params.size(); i++) { + tip.append(params.get(i).getName()); + tip.append(" "); + } + setTailTip(tip.toString()); + } else if (params.get(params.size() - 1).getName().charAt(0) == '[') { + setTailTip(params.get(params.size() - 1).getName()); + desc = params.get(params.size() - 1).getDescription(); + } + } else { + setTailTip(""); + if (tipType != TipType.TAIL_TIP){ + setSuggestionType(SuggestionType.COMPLETER); + } + } + doDescription(desc); + } + return true; + } + + private void doDescription(List desc) { + if (descriptionSize == 0) { + return; + } + if (desc.isEmpty()) { + clearDescription(); + } else if (desc.size() == descriptionSize) { + addDescription(desc); + } else if (desc.size() > descriptionSize) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.append(desc.get(descriptionSize - 1)).append("...", new AttributedStyle(AttributedStyle.INVERSE)); + List mod = new ArrayList<>(desc.subList(0, descriptionSize-1)); + mod.add(asb.toAttributedString()); + addDescription(mod); + } else if (desc.size() < descriptionSize) { + while (desc.size() != descriptionSize) { + desc.add(new AttributedString("")); + } + addDescription(desc); + } + } + + public boolean toggleKeyBindings() { + if (autosuggestion) { + defaultBindings(); + } else { + autosuggestionBindings(); + } + return autosuggestion; + } + + /* + * key bindings... + * + */ + public void autosuggestionBindings() { + if (autosuggestion) { + return; + } + KeyMap map = getKeyMap(LineReader.MAIN); + for (Map.Entry> entry : defaultBindings.entrySet()) { + if (entry.getKey().name().equals(LineReader.ACCEPT_LINE)) { + for (String s: entry.getValue()) { + map.bind(new Reference("_tailtip-accept-line"), s); + } + } + if (entry.getKey().name().equals(LineReader.BACKWARD_DELETE_CHAR)) { + for (String s: entry.getValue()) { + map.bind(new Reference("_tailtip-backward-delete-char"), s); + } + } + if (entry.getKey().name().equals(LineReader.DELETE_CHAR)) { + for (String s: entry.getValue()) { + map.bind(new Reference("_tailtip-delete-char"), s); + } + } + if (entry.getKey().name().equals(LineReader.EXPAND_OR_COMPLETE)) { + for (String s: entry.getValue()) { + map.bind(new Reference("_tailtip-expand-or-complete"), s); + } + } + } + map.bind(new Reference("_tailtip-insert"), " "); + if (tipType != TipType.TAIL_TIP) { + setSuggestionType(SuggestionType.COMPLETER); + } else { + setSuggestionType(SuggestionType.TAIL_TIP); + } + autosuggestion = true; + } + + public void defaultBindings() { + if (!autosuggestion) { + return; + } + KeyMap map = getKeyMap(LineReader.MAIN); + for (Map.Entry> entry : defaultBindings.entrySet()) { + for (String s: entry.getValue()) { + map.bind(entry.getKey(), s); + } + } + map.bind(new Reference(LineReader.SELF_INSERT), " "); + setSuggestionType(SuggestionType.NONE); + autosuggestion = false; + } + } + + public static class ArgDesc { + private String name; + private List description = new ArrayList(); + + public ArgDesc(String name) { + this(name, new ArrayList()); + } + + public ArgDesc(String name, List description) { + this.name = name; + this.description = new ArrayList<>(description); + } + + public String getName() { + return name; + } + + public List getDescription() { + return description; + } + + public static List doArgNames(List names) { + List out = new ArrayList<>(); + for (String n: names) { + out.add(new ArgDesc(n)); + } + return out; + } + } + +} diff --git a/builtins/src/test/java/org/jline/example/Example.java b/builtins/src/test/java/org/jline/example/Example.java index 3ad821323..577ac8220 100644 --- a/builtins/src/test/java/org/jline/example/Example.java +++ b/builtins/src/test/java/org/jline/example/Example.java @@ -24,9 +24,14 @@ import org.jline.builtins.Completers.TreeCompleter; import org.jline.builtins.Options.HelpException; import org.jline.builtins.TTop; +import org.jline.builtins.Widgets.AutopairWidgets; +import org.jline.builtins.Widgets.AutosuggestionWidgets; +import org.jline.builtins.Widgets.TailTipWidgets; +import org.jline.builtins.Widgets.TailTipWidgets.TipType; +import org.jline.builtins.Widgets.ArgDesc; import org.jline.keymap.KeyMap; import org.jline.reader.*; -import org.jline.reader.LineReader.Option; +import org.jline.reader.LineReader.SuggestionType; import org.jline.reader.impl.DefaultParser; import org.jline.reader.impl.DefaultParser.Bracket; import org.jline.reader.impl.LineReaderImpl; @@ -56,7 +61,7 @@ public static void usage() { , " -system terminalBuilder.system(false)" , " +system terminalBuilder.system(true)" , " Completors:" - , " argumet an argument completor" + , " argumet an argument completor & autosuggestion" , " files a completor that completes file names" , " none no completors" , " param a paramenter completer using Java functional interface" @@ -80,35 +85,37 @@ public static void usage() { System.out.println(u); } } - + public static void help() { String[] help = { "List of available commands:" , " Builtin:" - , " complete UNAVAILABLE" - , " history list history of commands" - , " keymap manipulate keymaps" - , " less file pager" - , " nano nano editor" - , " setopt set options" - , " tmux UNAVAILABLE" - , " ttop display and update sorted information about threads" - , " unsetopt unset options" - , " widget UNAVAILABLE" + , " complete UNAVAILABLE" + , " history list history of commands" + , " keymap manipulate keymaps" + , " less file pager" + , " nano nano editor" + , " setopt set options" + , " tmux UNAVAILABLE" + , " ttop display and update sorted information about threads" + , " unsetopt unset options" + , " widget UNAVAILABLE" + , " autopair toggle brackets/quotes autopair key bindings" + , " autosuggestion history, completer, tailtip [tailtip|completer|combined] or none" , " Example:" - , " cls clear screen" - , " help list available commands" - , " exit exit from example app" - , " set set lineReader variable" - , " sleep sleep 3 seconds" - , " testkey display key events" - , " tput set terminal capability" + , " cls clear screen" + , " help list available commands" + , " exit exit from example app" + , " set set lineReader variable" + , " sleep sleep 3 seconds" + , " testkey display key events" + , " tput set terminal capability" , " Additional help:" , " --help"}; for (String u: help) { System.out.println(u); } - + } public static void main(String[] args) throws IOException { @@ -121,7 +128,7 @@ public static void main(String[] args) throws IOException { boolean timer = false; TerminalBuilder builder = TerminalBuilder.builder(); - + if ((args == null) || (args.length == 0)) { usage(); @@ -198,7 +205,14 @@ public static void main(String[] args) throws IOException { break; case "argument": completer = new ArgumentCompleter( - new StringsCompleter("foo11", "foo12", "foo13"), + new Completer() { + @Override + public void complete(LineReader reader, ParsedLine line, List candidates) { + candidates.add(new Candidate("foo11", "foo11", null, "with complete argDesc", null, null, true)); + candidates.add(new Candidate("foo12", "foo12", null, "with argDesc -names only", null, null, true)); + candidates.add(new Candidate("foo13", "foo13", null, "-", null, null, true)); + } + }, new StringsCompleter("foo21", "foo22", "foo23"), new Completer() { @Override @@ -290,8 +304,8 @@ public void complete(LineReader reader, ParsedLine line, List candida } } } - - Terminal terminal = builder.build(); + + Terminal terminal = builder.build(); System.out.println(terminal.getName()+": "+terminal.getType()); System.out.println("\nhelp: list available commands"); LineReader reader = LineReaderBuilder.builder() @@ -302,7 +316,25 @@ public void complete(LineReader reader, ParsedLine line, List candida .variable(LineReader.INDENTATION, 2) .option(Option.INSERT_BRACKET, true) .build(); - + AutopairWidgets autopairWidgets = new AutopairWidgets(reader); + AutosuggestionWidgets autosuggestionWidgets = new AutosuggestionWidgets(reader); + Map> tailTips = new HashMap<>(); + tailTips.put("foo12", ArgDesc.doArgNames(Arrays.asList("param1", "param2", "[paramN...]"))); + tailTips.put("foo11", Arrays.asList( + new ArgDesc("param1",Arrays.asList(new AttributedString("Param1 description...") + , new AttributedString("line 2: This is a very long line that does exceed the terminal width." + +" The line will be truncated automatically (by Status class) be before printing out.") + , new AttributedString("line 3") + , new AttributedString("line 4") + , new AttributedString("line 5") + , new AttributedString("line 6") + )) + , new ArgDesc("param2",Arrays.asList(new AttributedString("Param2 description...") + , new AttributedString("line 2") + )) + , new ArgDesc("param3", new ArrayList<>()) + )); + TailTipWidgets tailtipWidgets = new TailTipWidgets(reader, tailTips, TipType.COMPLETER); if (timer) { Executors.newScheduledThreadPool(1) .scheduleAtFixedRate(() -> { @@ -405,7 +437,7 @@ else if ("sleep".equals(pl.word())) { } else if ("tmux".equals(pl.word())) { Commands.tmux(terminal, System.out, System.err, - null, //Supplier getter, + null, //Supplier getter, null, //Consumer setter, null, //Consumer runner, argv); @@ -445,6 +477,56 @@ else if ("unsetopt".equals(pl.word())) { else if ("ttop".equals(pl.word())) { TTop.ttop(terminal, System.out, System.err, argv); } + else if ("autopair".equals(pl.word())) { + terminal.writer().print("Autopair widgets are "); + if (autopairWidgets.toggleKeyBindings()) { + terminal.writer().println("bounded."); + } else { + terminal.writer().println("unbounded."); + } + } + else if ("autosuggestion".equals(pl.word())) { + if (pl.words().size() > 1) { + String type = pl.words().get(1).toLowerCase(); + if (type.startsWith("his")) { + tailtipWidgets.defaultBindings(); + autosuggestionWidgets.autosuggestionBindings(); + } else if (type.startsWith("tai")) { + autosuggestionWidgets.defaultBindings(); + tailtipWidgets.autosuggestionBindings(); + tailtipWidgets.setDescriptionSize(5); + if (pl.words().size() > 2) { + String mode = pl.words().get(2).toLowerCase(); + if (mode.startsWith("tai")) { + tailtipWidgets.setTipType(TipType.TAIL_TIP); + } else if (mode.startsWith("comp")) { + tailtipWidgets.setTipType(TipType.COMPLETER); + } else if (mode.startsWith("comb")) { + tailtipWidgets.setTipType(TipType.COMBINED); + } + } + } else if (type.startsWith("com")) { + autosuggestionWidgets.defaultBindings(); + tailtipWidgets.defaultBindings(); + reader.setAutosuggestion(SuggestionType.COMPLETER); + } else if (type.startsWith("non")) { + autosuggestionWidgets.defaultBindings(); + tailtipWidgets.defaultBindings(); + reader.setAutosuggestion(SuggestionType.NONE); + } else { + terminal.writer().println("Usage: autosuggestion history|completer|tailtip|none"); + } + } else { + if (tailtipWidgets.isActive()) { + terminal.writer().println("Autosuggestion: tailtip/" + tailtipWidgets.getTipType()); + } else { + terminal.writer().println("Autosuggestion: " + reader.getAutosuggestion()); + } + } + if (!tailtipWidgets.isActive()) { + Status.getStatus(terminal).update(null); + } + } else if ("help".equals(pl.word()) || "?".equals(pl.word())) { help(); } diff --git a/reader/src/main/java/org/jline/reader/LineReader.java b/reader/src/main/java/org/jline/reader/LineReader.java index fbd41dfcc..b2dddc6b1 100644 --- a/reader/src/main/java/org/jline/reader/LineReader.java +++ b/reader/src/main/java/org/jline/reader/LineReader.java @@ -450,6 +450,13 @@ enum RegionType { PASTE } + enum SuggestionType { + NONE, + HISTORY, + COMPLETER, + TAIL_TIP + } + /** * Read the next line and return the contents of the buffer. * @@ -670,4 +677,13 @@ enum RegionType { void editAndAddInBuffer(File file) throws Exception; + String getLastBinding(); + + String getTailTip(); + + void setTailTip(String tailTip); + + void setAutosuggestion(SuggestionType type); + + SuggestionType getAutosuggestion(); } diff --git a/reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java b/reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java index 2efbf36d7..5766925c6 100644 --- a/reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java +++ b/reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java @@ -117,7 +117,7 @@ protected enum State { */ DONE, /** - * readLine should exit return empty String + * readLine should exit and return empty String */ IGNORE, /** @@ -171,6 +171,8 @@ protected enum BellType { protected final Map options = new HashMap<>(); protected final Buffer buf = new BufferImpl(); + protected String tailTip = ""; + protected SuggestionType autosuggestion = SuggestionType.NONE; protected final Size size = new Size(); @@ -186,6 +188,7 @@ protected enum BellType { protected boolean searchFailing; protected boolean searchBackward; protected int searchIndex = -1; + protected boolean inCompleterMenu; // Reading buffers @@ -327,6 +330,26 @@ public Buffer getBuffer() { return buf; } + @Override + public void setAutosuggestion(SuggestionType type) { + this.autosuggestion = type; + } + + @Override + public SuggestionType getAutosuggestion() { + return autosuggestion; + } + + @Override + public String getTailTip(){ + return tailTip; + } + + @Override + public void setTailTip(String tailTip) { + this.tailTip = tailTip; + } + @Override public void runMacro(String macro) { bindingReader.runMacro(macro); @@ -487,7 +510,7 @@ public String readLine(String prompt, String rightPrompt, MaskingCallback maskin // buffer may be null if (!commandsBuffer.isEmpty()) { String cmd = commandsBuffer.remove(0); - boolean done = false; + boolean done = false; do { try { parser.parse(cmd, cmd.length() + 1, ParseContext.ACCEPT_LINE); @@ -928,10 +951,12 @@ public ParsedLine getParsedLine() { return parsedLine; } + @Override public String getLastBinding() { return bindingReader.getLastBinding(); } + @Override public String getSearchTerm() { return searchTerm != null ? searchTerm.toString() : null; } @@ -1042,7 +1067,7 @@ public void editAndAddInBuffer(File file) throws Exception { } br.close(); } - + // // Widget implementation // @@ -3913,6 +3938,38 @@ private void concat(List lines, AttributedStringBuilder sb) { sb.append(lines.get(lines.size() - 1)); } + private String matchPreviousCommand(String buffer) { + if (buffer.length() == 0) { + return ""; + } + History history = getHistory(); + StringBuilder sb = new StringBuilder(); + char prev = '0'; + for (char c: buffer.toCharArray()) { + if ((c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c == '}' || c == '^') && prev != '\\' ) { + sb.append('\\'); + } + sb.append(c); + prev = c; + } + Pattern pattern = Pattern.compile(sb.toString() + ".*", Pattern.DOTALL); + Iterator iter = history.reverseIterator(history.last()); + String suggestion = ""; + int tot = 0; + while (iter.hasNext()) { + History.Entry entry = iter.next(); + Matcher matcher = pattern.matcher(entry.line()); + if (matcher.matches()) { + suggestion = entry.line().substring(buffer.length()); + break; + } else if (tot > 200) { + break; + } + tot++; + } + return suggestion; + } + /** * Compute the full string to be displayed with the left, right and secondary prompts * @param secondaryPrompts a list to store the secondary prompts @@ -3925,13 +3982,60 @@ public AttributedString getDisplayedBufferWithPrompts(List sec AttributedStringBuilder full = new AttributedStringBuilder().tabs(TAB_WIDTH); full.append(prompt); full.append(tNewBuf); + if (!inCompleterMenu) { + String lastBinding = getLastBinding() != null ? getLastBinding() : ""; + if (autosuggestion == SuggestionType.HISTORY) { + AttributedStringBuilder sb = new AttributedStringBuilder(); + tailTip = matchPreviousCommand(buf.toString()); + sb.styled(AttributedStyle::faint, tailTip); + full.append(sb.toAttributedString()); + } else if (autosuggestion == SuggestionType.COMPLETER) { + if (buf.length() > 0 && buf.length() == buf.cursor() + && (!lastBinding.equals("\t") || buf.prevChar() == ' ')) { + clearChoices(); + listChoices(true); + } else if (!lastBinding.equals("\t")){ + clearChoices(); + clearStatus(); + } + } else if (autosuggestion == SuggestionType.TAIL_TIP) { + if (buf.length() == buf.cursor()) { + if (!lastBinding.equals("\t")){ + clearChoices(); + } + AttributedStringBuilder sb = new AttributedStringBuilder(); + if (buf.prevChar() != ' ') { + if (!tailTip.startsWith("[")) { + int idx = tailTip.indexOf(' '); + if (idx > 0) { + tailTip = tailTip.substring(idx); + } + } else { + sb.append(" "); + } + } + sb.styled(AttributedStyle::faint, tailTip); + full.append(sb.toAttributedString()); + } else { + clearStatus(); + } + } + } if (post != null) { full.append("\n"); full.append(post.get()); } + inCompleterMenu = false; return full.toAttributedString(); } + private void clearStatus() { + Status status = Status.getStatus(terminal, false); + if (status != null) { + status.clear(); + } + } + private AttributedString getHighlightedBuffer(String buffer) { if (maskingCallback != null) { buffer = maskingCallback.display(buffer); @@ -4224,7 +4328,11 @@ protected boolean completePrefix() { } protected boolean listChoices() { - return doComplete(CompletionType.List, isSet(Option.MENU_COMPLETE), false); + return listChoices(false); + } + + private boolean listChoices(boolean forSuggestion) { + return doComplete(CompletionType.List, isSet(Option.MENU_COMPLETE), false, forSuggestion); } protected boolean deleteCharOrList() { @@ -4236,6 +4344,10 @@ protected boolean deleteCharOrList() { } protected boolean doComplete(CompletionType lst, boolean useMenu, boolean prefix) { + return doComplete(lst, useMenu, prefix, false); + } + + protected boolean doComplete(CompletionType lst, boolean useMenu, boolean prefix, boolean forSuggestion) { // If completion is disabled, just bail out if (getBoolean(DISABLE_COMPLETION, false)) { return true; @@ -4362,7 +4474,7 @@ protected boolean doComplete(CompletionType lst, boolean useMenu, boolean prefix List possible = matching.entrySet().stream() .flatMap(e -> e.getValue().stream()) .collect(Collectors.toList()); - doList(possible, line.word(), false, line::escape); + doList(possible, line.word(), false, line::escape, forSuggestion); return !possible.isEmpty(); } @@ -4826,12 +4938,24 @@ && getLastBinding().charAt(0) != ' ' return true; } } + inCompleterMenu = true; redisplay(); } return false; } - protected boolean doList(List possible, String completed, boolean runLoop, BiFunction escaper) { + protected boolean clearChoices() { + return doList(new ArrayList(), "", false, null, false); + } + + protected boolean doList(List possible + , String completed, boolean runLoop, BiFunction escaper) { + return doList(possible, completed, runLoop, escaper, false); + } + + protected boolean doList(List possible + , String completed + , boolean runLoop, BiFunction escaper, boolean forSuggestion) { // If we list only and if there's a big // number of items, we should ask the user // for confirmation, display the list @@ -4844,13 +4968,17 @@ protected boolean doList(List possible, String completed, boolean run int listMax = getInt(LIST_MAX, DEFAULT_LIST_MAX); if (listMax > 0 && possible.size() >= listMax || lines >= size.getRows() - promptLines) { - // prompt - post = () -> new AttributedString(getAppName() + ": do you wish to see all " + possible.size() - + " possibilities (" + lines + " lines)?"); - redisplay(true); - int c = readCharacter(); - if (c != 'y' && c != 'Y' && c != '\t') { - post = null; + if (!forSuggestion) { + // prompt + post = () -> new AttributedString(getAppName() + ": do you wish to see all " + possible.size() + + " possibilities (" + lines + " lines)?"); + redisplay(true); + int c = readCharacter(); + if (c != 'y' && c != 'Y' && c != '\t') { + post = null; + return false; + } + } else { return false; } } diff --git a/terminal/src/main/java/org/jline/terminal/impl/AbstractWindowsTerminal.java b/terminal/src/main/java/org/jline/terminal/impl/AbstractWindowsTerminal.java index 3523a8deb..586d61353 100644 --- a/terminal/src/main/java/org/jline/terminal/impl/AbstractWindowsTerminal.java +++ b/terminal/src/main/java/org/jline/terminal/impl/AbstractWindowsTerminal.java @@ -216,7 +216,9 @@ public void setSize(Size size) { public void close() throws IOException { super.close(); closing = true; - pump.interrupt(); + if (pump != null) { + pump.interrupt(); + } ShutdownHooks.remove(closer); for (Map.Entry entry : nativeHandlers.entrySet()) { Signals.unregister(entry.getKey().name(), entry.getValue()); diff --git a/terminal/src/main/java/org/jline/utils/Status.java b/terminal/src/main/java/org/jline/utils/Status.java index c38b95d50..dc7b57ea7 100644 --- a/terminal/src/main/java/org/jline/utils/Status.java +++ b/terminal/src/main/java/org/jline/utils/Status.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2002-2018, the original author or authors. + * Copyright (c) 2002-2019, the original author or authors. * * This software is distributable under the BSD license. See the terms of the * BSD license in the documentation provided with this software. @@ -13,8 +13,6 @@ import java.util.ArrayList; import java.util.List; import org.jline.terminal.Terminal; -import org.jline.terminal.Terminal.Signal; -import org.jline.terminal.Terminal.SignalHandler; import org.jline.terminal.impl.AbstractTerminal; import org.jline.utils.InfoCmp.Capability; import org.jline.terminal.Size; @@ -29,6 +27,8 @@ public class Status { protected int columns; protected boolean force; protected boolean suspended = false; + protected AttributedString borderString; + protected int border = 0; public static Status getStatus(Terminal terminal) { return getStatus(terminal, true); @@ -48,10 +48,21 @@ public Status(AbstractTerminal terminal) { && terminal.getStringCapability(Capability.restore_cursor) != null && terminal.getStringCapability(Capability.cursor_address) != null; if (supported) { + char borderChar = '─'; + AttributedStringBuilder bb = new AttributedStringBuilder(); + for (int i = 0; i < 200; i++) { + bb.append(borderChar); + } + borderString = bb.toAttributedString(); resize(); } } + public void setBorder(boolean border) { + clear(); + this.border = border ? 1 : 0; + } + public void resize() { Size size = terminal.getSize(); this.rows = size.getRows(); @@ -79,6 +90,26 @@ public void redraw() { update(oldLines); } + public void clear() { + privateClear(oldLines.size()); + } + + private void clearAll() { + int b = border; + border = 0; + privateClear(oldLines.size() + b); + } + + private void privateClear(int statusSize) { + List as = new ArrayList<>(); + for (int i = 0; i < statusSize; i++) { + as.add(new AttributedString("")); + } + if (!as.isEmpty()) { + update(as); + } + } + public void update(List lines) { if (!supported) { return; @@ -90,10 +121,14 @@ public void update(List lines) { linesToRestore = new ArrayList<>(lines); return; } + if (lines.isEmpty()) { + clearAll(); + } if (oldLines.equals(lines) && !force) { return; } - int nb = lines.size() - oldLines.size(); + int statusSize = lines.size() + (lines.size() == 0 ? 0 : border); + int nb = statusSize - oldLines.size() - (oldLines.size() == 0 ? 0 : border); if (nb > 0) { for (int i = 0; i < nb; i++) { terminal.puts(Capability.cursor_down); @@ -103,13 +138,23 @@ public void update(List lines) { } } terminal.puts(Capability.save_cursor); - terminal.puts(Capability.cursor_address, rows - lines.size(), 0); + terminal.puts(Capability.cursor_address, rows - statusSize, 0); terminal.puts(Capability.clr_eos); + if (border == 1 && lines.size() > 0) { + terminal.puts(Capability.cursor_address, rows - statusSize, 0); + borderString.columnSubSequence(0, columns).print(terminal); + } for (int i = 0; i < lines.size(); i++) { terminal.puts(Capability.cursor_address, rows - lines.size() + i, 0); - lines.get(i).columnSubSequence(0, columns).print(terminal); + if (lines.get(i).length() > columns) { + AttributedStringBuilder asb = new AttributedStringBuilder(); + asb.append(lines.get(i).substring(0, columns - 3)).append("...", new AttributedStyle(AttributedStyle.INVERSE)); + asb.toAttributedString().columnSubSequence(0, columns).print(terminal); + } else { + lines.get(i).columnSubSequence(0, columns).print(terminal); + } } - terminal.puts(Capability.change_scroll_region, 0, rows - 1 - lines.size()); + terminal.puts(Capability.change_scroll_region, 0, rows - 1 - statusSize); terminal.puts(Capability.restore_cursor); terminal.flush(); oldLines = new ArrayList<>(lines); @@ -128,14 +173,14 @@ public void suspend() { public void restore() { if (!suspended) { return; - } + } suspended = false; update(linesToRestore); linesToRestore = Collections.emptyList(); } public int size() { - return oldLines.size(); + return oldLines.size() + border; } -} \ No newline at end of file +}