org.jline.widget.AutopairWidgets Maven / Gradle / Ivy
The newest version!
/*
* Copyright (c) 2002-2020, the original author(s).
*
* 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.widget;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jline.keymap.KeyMap;
import org.jline.reader.Binding;
import org.jline.reader.Buffer;
import org.jline.reader.LineReader;
import org.jline.reader.Reference;
/**
* Creates and manages widgets that auto-closes, deletes and skips over matching delimiters intelligently.
*
* @author Matti Rinta-Nikkola
*/
public 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 enabled;
{
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);
}
@SuppressWarnings("this-escape")
public AutopairWidgets(LineReader reader, boolean addCurlyBrackets) {
super(reader);
if (existsWidget(AP_INSERT)) {
throw new IllegalStateException("AutopairWidgets already created!");
}
if (addCurlyBrackets) {
pairs.put("{", "}");
}
addWidget(AP_INSERT, this::autopairInsert);
addWidget("_autopair-close", this::autopairClose);
addWidget(AP_BACKWARD_DELETE_CHAR, this::autopairDelete);
addWidget(AUTOPAIR_TOGGLE, this::toggleKeyBindings);
KeyMap map = getKeyMap();
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()));
}
}
}
public void enable() {
if (!enabled) {
toggle();
}
}
public void disable() {
if (enabled) {
toggle();
}
}
public boolean toggle() {
boolean before = enabled;
toggleKeyBindings();
return !before;
}
/*
* 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 (enabled) {
defaultBindings();
} else {
customBindings();
}
return enabled;
}
/*
* key bindings...
*
*/
private void customBindings() {
boolean ttActive = tailtipEnabled();
if (ttActive) {
callWidget(TAILTIP_TOGGLE);
}
KeyMap map = getKeyMap();
for (Map.Entry p : pairs.entrySet()) {
map.bind(new Reference(AP_INSERT), p.getKey());
if (!p.getKey().equals(p.getValue())) {
map.bind(new Reference("_autopair-close"), p.getValue());
}
}
aliasWidget(AP_BACKWARD_DELETE_CHAR, LineReader.BACKWARD_DELETE_CHAR);
if (ttActive) {
callWidget(TAILTIP_TOGGLE);
}
enabled = true;
}
private void defaultBindings() {
KeyMap map = getKeyMap();
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());
}
}
aliasWidget("." + LineReader.BACKWARD_DELETE_CHAR, LineReader.BACKWARD_DELETE_CHAR);
if (tailtipEnabled()) {
callWidget(TAILTIP_TOGGLE);
callWidget(TAILTIP_TOGGLE);
}
enabled = false;
}
/*
* helpers
*/
private boolean tailtipEnabled() {
return getWidget(LineReader.ACCEPT_LINE).equals(TT_ACCEPT_LINE);
}
private boolean canPair(String d) {
if (balanced(d) && !nexToBoundary(d)) {
return !d.equals(" ") || (!prevChar().equals(" ") && !currChar().equals(" "));
}
return false;
}
private boolean canSkip(String d) {
return pairs.get(d).equals(d) && d.charAt(0) != ' ' && currChar().equals(d) && balanced(d);
}
private boolean canDelete(String d) {
return balanced(d);
}
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) {
return (lb.length() > 0 && prevChar().matches(lb))
|| (rb.length() > 0 && currChar().matches(rb));
}
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;
}
}