diff --git a/reader/src/main/java/org/jline/keymap/BindingReader.java b/reader/src/main/java/org/jline/keymap/BindingReader.java index 2a796b814..1c9a175de 100644 --- a/reader/src/main/java/org/jline/keymap/BindingReader.java +++ b/reader/src/main/java/org/jline/keymap/BindingReader.java @@ -117,6 +117,33 @@ public T readBinding(KeyMap keys, KeyMap local, boolean block) { return null; } + public String readStringUntil(String sequence) { + StringBuilder sb = new StringBuilder(); + if (!pushBackChar.isEmpty()) { + pushBackChar.forEach(sb::appendCodePoint); + } + try { + char[] buf = new char[64]; + while (true) { + int idx = sb.indexOf(sequence, Math.max(0, sb.length() - buf.length - sequence.length())); + if (idx >= 0) { + String rem = sb.substring(idx + sequence.length()); + runMacro(rem); + return sb.substring(0, idx); + } + int l = reader.readBuffered(buf); + if (l < 0) { + throw new ClosedException(); + } + sb.append(buf, 0, l); + } + } catch (ClosedException e) { + throw new EndOfFileException(e); + } catch (IOException e) { + throw new IOError(e); + } + } + /** * Read a codepoint from the terminal. * @@ -144,6 +171,47 @@ public int readCharacter() { } } + public int readCharacterBuffered() { + try { + if (pushBackChar.isEmpty()) { + char[] buf = new char[32]; + int l = reader.readBuffered(buf); + if (l <= 0) { + return -1; + } + int s = 0; + for (int i = 0; i < l; ) { + int c = buf[i++]; + if (Character.isHighSurrogate((char) c)) { + s = c; + if (i < l) { + c = buf[i++]; + pushBackChar.addLast(Character.toCodePoint((char) s, (char) c)); + } else { + break; + } + } else { + s = 0; + pushBackChar.addLast(c); + } + } + if (s != 0) { + int c = reader.read(); + if (c >= 0) { + pushBackChar.addLast(Character.toCodePoint((char) s, (char) c)); + } else { + return -1; + } + } + } + return pushBackChar.pop(); + } catch (ClosedException e) { + throw new EndOfFileException(e); + } catch (IOException e) { + throw new IOError(e); + } + } + public int peekCharacter(long timeout) { if (!pushBackChar.isEmpty()) { return pushBackChar.peek(); 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 cc7ceef0a..9e3f48bdd 100644 --- a/reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java +++ b/reader/src/main/java/org/jline/reader/impl/LineReaderImpl.java @@ -914,6 +914,19 @@ protected T doReadBinding(KeyMap keys, KeyMap local) { } } + protected String doReadStringUntil(String sequence) { + if (lock.isHeldByCurrentThread()) { + try { + lock.unlock(); + return bindingReader.readStringUntil(sequence); + } finally { + lock.lock(); + } + } else { + return bindingReader.readStringUntil(sequence); + } + } + /** * Read from the input stream and decode an operation from the key map. * @@ -5625,28 +5638,10 @@ public boolean mouse() { } public boolean beginPaste() { - final Object SELF_INSERT = new Object(); - final Object END_PASTE = new Object(); - KeyMap keyMap = new KeyMap<>(); - keyMap.setUnicode(SELF_INSERT); - keyMap.setNomatch(SELF_INSERT); - keyMap.setAmbiguousTimeout(0); - keyMap.bind(END_PASTE, BRACKETED_PASTE_END); - StringBuilder sb = new StringBuilder(); - while (true) { - Object b = doReadBinding(keyMap, null); - if (b == END_PASTE) { - break; - } - String s = getLastBinding(); - if ("\r".equals(s)) { - s = "\n"; - } - sb.append(s); - } + String str = doReadStringUntil(BRACKETED_PASTE_END); regionActive = RegionType.PASTE; regionMark = getBuffer().cursor(); - getBuffer().write(sb); + getBuffer().write(str.replace('\r', '\n')); return true; } diff --git a/reader/src/test/java/org/jline/keymap/BindingReaderTest.java b/reader/src/test/java/org/jline/keymap/BindingReaderTest.java index 8be061d6e..8bfb6ee89 100644 --- a/reader/src/test/java/org/jline/keymap/BindingReaderTest.java +++ b/reader/src/test/java/org/jline/keymap/BindingReaderTest.java @@ -74,4 +74,12 @@ public void testBindingReaderUnicode() { assertEquals("b", reader.getLastBinding()); assertNull(reader.readBinding(keyMap)); } + + @Test + public void testBindingReaderReadString() { + in.setIn(new ByteArrayInputStream("\uD834\uDD21abc0123456789defg".getBytes(StandardCharsets.UTF_8))); + BindingReader reader = new BindingReader(terminal.reader()); + String str = reader.readStringUntil("fg"); + assertEquals("\uD834\uDD21abc0123456789de", str); + } } diff --git a/terminal/src/main/java/org/jline/terminal/impl/AbstractPty.java b/terminal/src/main/java/org/jline/terminal/impl/AbstractPty.java index 3b52e4a71..2ffe4bd45 100644 --- a/terminal/src/main/java/org/jline/terminal/impl/AbstractPty.java +++ b/terminal/src/main/java/org/jline/terminal/impl/AbstractPty.java @@ -86,6 +86,11 @@ public int read(long timeout, boolean isPeek) throws IOException { } } + @Override + public int readBuffered(byte[] b) throws IOException { + return in.read(b); + } + private void setNonBlocking() { if (current == null || current.getControlChar(Attributes.ControlChar.VMIN) != 0 diff --git a/terminal/src/main/java/org/jline/utils/NonBlocking.java b/terminal/src/main/java/org/jline/utils/NonBlocking.java index c70c1ec32..728cbb218 100644 --- a/terminal/src/main/java/org/jline/utils/NonBlocking.java +++ b/terminal/src/main/java/org/jline/utils/NonBlocking.java @@ -197,6 +197,33 @@ protected int read(long timeout, boolean isPeek) throws IOException { } } + @Override + public int readBuffered(char[] b) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if (b.length == 0) { + return 0; + } else { + if (chars.hasRemaining()) { + int r = Math.min(b.length, chars.remaining()); + chars.get(b); + return r; + } else { + byte[] buf = new byte[b.length]; + int l = input.readBuffered(buf); + if (l < 0) { + return l; + } else { + ByteBuffer bytes = ByteBuffer.wrap(buf, 0, l); + CharBuffer chars = CharBuffer.wrap(b); + decoder.decode(bytes, chars, false); + chars.flip(); + return chars.remaining(); + } + } + } + } + @Override public void shutdown() { input.shutdown(); diff --git a/terminal/src/main/java/org/jline/utils/NonBlockingInputStream.java b/terminal/src/main/java/org/jline/utils/NonBlockingInputStream.java index e5f1cfbfb..1266b908d 100644 --- a/terminal/src/main/java/org/jline/utils/NonBlockingInputStream.java +++ b/terminal/src/main/java/org/jline/utils/NonBlockingInputStream.java @@ -78,6 +78,16 @@ public int read(byte b[], int off, int len) throws IOException { return 1; } + public int readBuffered(byte[] b) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if (b.length == 0) { + return 0; + } else { + return super.read(b, 0, b.length); + } + } + /** * Shuts down the thread that is handling blocking I/O if any. Note that if the * thread is currently blocked waiting for I/O it may not actually diff --git a/terminal/src/main/java/org/jline/utils/NonBlockingPumpReader.java b/terminal/src/main/java/org/jline/utils/NonBlockingPumpReader.java index 4e845c79a..d9e91750e 100644 --- a/terminal/src/main/java/org/jline/utils/NonBlockingPumpReader.java +++ b/terminal/src/main/java/org/jline/utils/NonBlockingPumpReader.java @@ -106,6 +106,28 @@ protected synchronized int read(long timeout, boolean isPeek) throws IOException return res; } + @Override + public int readBuffered(char[] b) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if (b.length == 0) { + return 0; + } else { + int r = Math.min(b.length, readBuffer.remaining()); + if (r > 0) { + readBuffer.get(b); + return r; + } else { + r = read(-1, false); + if (r >= 0) { + b[0] = (char) r; + return 1; + } + return r; + } + } + } + synchronized void write(char[] cbuf, int off, int len) throws IOException { while (len > 0) { // Blocks until there is new space available for buffering or the diff --git a/terminal/src/main/java/org/jline/utils/NonBlockingReader.java b/terminal/src/main/java/org/jline/utils/NonBlockingReader.java index 2aeee53df..fc92edf34 100644 --- a/terminal/src/main/java/org/jline/utils/NonBlockingReader.java +++ b/terminal/src/main/java/org/jline/utils/NonBlockingReader.java @@ -85,6 +85,8 @@ public int read(char[] b, int off, int len) throws IOException { return 1; } + public abstract int readBuffered(char[] b) throws IOException; + public int available() { return 0; } diff --git a/terminal/src/main/java/org/jline/utils/NonBlockingReaderImpl.java b/terminal/src/main/java/org/jline/utils/NonBlockingReaderImpl.java index 172ae1579..5045fd476 100644 --- a/terminal/src/main/java/org/jline/utils/NonBlockingReaderImpl.java +++ b/terminal/src/main/java/org/jline/utils/NonBlockingReaderImpl.java @@ -90,6 +90,34 @@ public synchronized boolean ready() throws IOException { return ch >= 0 || in.ready(); } + @Override + public int readBuffered(char[] b) throws IOException { + if (b == null) { + throw new NullPointerException(); + } else if (b.length == 0) { + return 0; + } else if (exception != null) { + assert ch == READ_EXPIRED; + IOException toBeThrown = exception; + exception = null; + throw toBeThrown; + } else if (ch >= -1) { + b[0] = (char) ch; + ch = READ_EXPIRED; + return 1; + } else if (!threadIsReading) { + return in.read(b); + } else { + int c = read(-1, false); + if (c >= 0) { + b[0] = (char) c; + return 1; + } else { + return -1; + } + } + } + /** * Attempts to read a character from the input stream for a specific * period of time.