// 
// Decompiled by Procyon v0.6.0
// 

package org.jline.builtins;

import java.io.BufferedWriter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.Optional;
import java.io.Reader;
import java.io.InputStreamReader;
import java.io.ByteArrayInputStream;
import org.mozilla.universalchardet.CharsetListener;
import org.mozilla.universalchardet.UniversalDetector;
import java.io.ByteArrayOutputStream;
import java.util.LinkedList;
import java.nio.charset.Charset;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import java.util.Collections;
import java.util.Collection;
import org.jline.terminal.MouseEvent;
import java.io.InputStream;
import java.io.Writer;
import java.io.OutputStream;
import java.nio.file.StandardCopyOption;
import java.nio.file.CopyOption;
import java.io.OutputStreamWriter;
import java.nio.file.StandardOpenOption;
import java.nio.file.OpenOption;
import java.nio.file.attribute.FileAttribute;
import java.nio.file.LinkOption;
import org.jline.terminal.impl.MouseSupport;
import org.jline.utils.Status;
import org.jline.utils.InfoCmp;
import java.util.EnumSet;
import java.util.Iterator;
import java.util.Arrays;
import java.io.BufferedReader;
import java.util.stream.Stream;
import java.nio.file.PathMatcher;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.Objects;
import java.nio.file.Files;
import java.nio.file.FileVisitOption;
import java.nio.file.Paths;
import java.nio.file.FileSystems;
import java.io.IOException;
import org.jline.terminal.Attributes;
import java.util.ArrayList;
import java.io.File;
import org.jline.utils.AttributedString;
import java.util.Map;
import java.util.LinkedHashMap;
import org.jline.keymap.KeyMap;
import java.util.List;
import java.nio.file.Path;
import org.jline.terminal.Size;
import org.jline.keymap.BindingReader;
import org.jline.utils.Display;
import org.jline.terminal.Terminal;
import org.jline.reader.Editor;

public class Nano implements Editor
{
    protected final Terminal terminal;
    protected final Display display;
    protected final BindingReader bindingReader;
    protected final Size size;
    protected final Path root;
    protected final int vsusp;
    private final List<Path> syntaxFiles;
    protected KeyMap<Operation> keys;
    public String title;
    public boolean printLineNumbers;
    public boolean wrapping;
    public boolean smoothScrolling;
    public boolean mouseSupport;
    public Terminal.MouseTracking mouseTracking;
    public boolean oneMoreLine;
    public boolean constantCursor;
    public boolean quickBlank;
    public int tabs;
    public String brackets;
    public String matchBrackets;
    public String punct;
    public String quoteStr;
    private boolean restricted;
    private String syntaxName;
    private boolean writeBackup;
    private boolean atBlanks;
    private boolean view;
    private boolean cut2end;
    private boolean tempFile;
    private String historyLog;
    private boolean tabsToSpaces;
    private boolean autoIndent;
    protected final List<Buffer> buffers;
    protected int bufferIndex;
    protected Buffer buffer;
    protected String message;
    protected String errorMessage;
    protected int nbBindings;
    protected LinkedHashMap<String, String> shortcuts;
    protected String editMessage;
    protected final StringBuilder editBuffer;
    protected boolean searchCaseSensitive;
    protected boolean searchRegexp;
    protected boolean searchBackwards;
    protected String searchTerm;
    protected int matchedLength;
    protected PatternHistory patternHistory;
    protected WriteMode writeMode;
    protected List<String> cutbuffer;
    protected boolean mark;
    protected boolean highlight;
    private boolean searchToReplace;
    protected boolean readNewBuffer;
    private boolean nanorcIgnoreErrors;
    private final boolean windowsTerminal;
    private boolean insertHelp;
    private boolean help;
    private Box suggestionBox;
    private Map<AttributedString, List<AttributedString>> suggestions;
    private int mouseX;
    private int mouseY;
    
    public static String[] usage() {
        return new String[] { "nano -  edit files", "Usage: nano [OPTIONS] [FILES]", "  -? --help                    Show help", "  -B --backup                  When saving a file, back up the previous version of it, using the current filename", "                               suffixed with a tilde (~).", "  -I --ignorercfiles           Don't look at the system's nanorc nor at the user's nanorc.", "  -Q --quotestr=regex          Set the regular expression for matching the quoting part of a line.", "  -T --tabsize=number          Set the size (width) of a tab to number columns.", "  -U --quickblank              Do quick status-bar blanking: status-bar messages will disappear after 1 keystroke.", "  -c --constantshow            Constantly show the cursor position on the status bar.", "  -e --emptyline               Do not use the line below the title bar, leaving it entirely blank.", "  -j --jumpyscrolling          Scroll the buffer contents per half-screen instead of per line.", "  -l --linenumbers             Display line numbers to the left of the text area.", "  -m --mouse                   Enable mouse support, if available for your system.", "  -$ --softwrap                Enable 'soft wrapping'. ", "  -a --atblanks                Wrap lines at whitespace instead of always at the edge of the screen.", "  -R --restricted              Restricted mode: don't allow suspending; don't allow a file to be appended to,", "                               prepended to, or saved under a different name if it already has one;", "                               and don't use backup files.", "  -Y --syntax=name             The name of the syntax highlighting to use.", "  -z --suspend                 Enable the ability to suspend nano using the system's suspend keystroke (usually ^Z).", "  -v --view                    Don't allow the contents of the file to be altered: read-only mode.", "  -k --cutfromcursor           Make the 'Cut Text' command cut from the current cursor position to the end of the line", "  -t --tempfile                Save a changed buffer without prompting (when exiting with ^X).", "  -H --historylog=name         Log search strings to file, so they can be retrieved in later sessions", "  -E --tabstospaces            Convert typed tabs to spaces.", "  -i --autoindent              Indent new lines to the previous line's indentation." };
    }
    
    public Nano(final Terminal terminal, final File root) {
        this(terminal, root.toPath());
    }
    
    public Nano(final Terminal terminal, final Path root) {
        this(terminal, root, null);
    }
    
    public Nano(final Terminal terminal, final Path root, final Options opts) {
        this(terminal, root, opts, null);
    }
    
    public Nano(final Terminal terminal, final Path root, final Options opts, final ConfigurationPath configPath) {
        this.syntaxFiles = new ArrayList<Path>();
        this.title = "JLine Nano 3.0.0";
        this.printLineNumbers = false;
        this.wrapping = false;
        this.smoothScrolling = true;
        this.mouseSupport = false;
        this.mouseTracking = Terminal.MouseTracking.Off;
        this.oneMoreLine = true;
        this.constantCursor = false;
        this.quickBlank = false;
        this.tabs = 4;
        this.brackets = "\"\u2019)>]}";
        this.matchBrackets = "(<[{)>]}";
        this.punct = "!.?";
        this.quoteStr = "^([ \\t]*[#:>\\|}])+";
        this.restricted = false;
        this.writeBackup = false;
        this.atBlanks = false;
        this.view = false;
        this.cut2end = false;
        this.tempFile = false;
        this.historyLog = null;
        this.tabsToSpaces = false;
        this.autoIndent = false;
        this.buffers = new ArrayList<Buffer>();
        this.errorMessage = null;
        this.nbBindings = 0;
        this.editBuffer = new StringBuilder();
        this.matchedLength = -1;
        this.patternHistory = new PatternHistory(null);
        this.writeMode = WriteMode.WRITE;
        this.cutbuffer = new ArrayList<String>();
        this.mark = false;
        this.highlight = true;
        this.searchToReplace = false;
        this.readNewBuffer = true;
        this.insertHelp = false;
        this.help = false;
        this.terminal = terminal;
        this.windowsTerminal = terminal.getClass().getSimpleName().endsWith("WinSysTerminal");
        this.root = root;
        this.display = new Display(terminal, true);
        this.bindingReader = new BindingReader(terminal.reader());
        this.size = new Size();
        final Attributes attrs = terminal.getAttributes();
        this.vsusp = attrs.getControlChar(Attributes.ControlChar.VSUSP);
        if (this.vsusp > 0) {
            attrs.setControlChar(Attributes.ControlChar.VSUSP, 0);
            terminal.setAttributes(attrs);
        }
        final Path nanorc = (configPath != null) ? configPath.getConfig("jnanorc") : null;
        final boolean ignorercfiles = opts != null && opts.isSet("ignorercfiles");
        if (nanorc != null && !ignorercfiles) {
            try {
                this.parseConfig(nanorc);
            }
            catch (final IOException e) {
                this.errorMessage = "Encountered error while reading config file: " + nanorc;
            }
        }
        else if (new File("/usr/share/nano").exists() && !ignorercfiles) {
            final PathMatcher pathMatcher = FileSystems.getDefault().getPathMatcher("glob:/usr/share/nano/*.nanorc");
            try (final Stream<Path> pathStream = Files.walk(Paths.get("/usr/share/nano", new String[0]), new FileVisitOption[0])) {
                final Stream<Path> stream = pathStream;
                final PathMatcher obj = pathMatcher;
                Objects.requireNonNull(obj);
                final Stream<Path> filter = stream.filter(obj::matches);
                final List<Path> syntaxFiles = this.syntaxFiles;
                Objects.requireNonNull(syntaxFiles);
                filter.forEach(syntaxFiles::add);
                this.nanorcIgnoreErrors = true;
            }
            catch (final IOException e2) {
                this.errorMessage = "Encountered error while reading nanorc files";
            }
        }
        if (opts != null) {
            this.restricted = opts.isSet("restricted");
            this.syntaxName = null;
            if (opts.isSet("syntax")) {
                this.syntaxName = opts.get("syntax");
                this.nanorcIgnoreErrors = false;
            }
            if (opts.isSet("backup")) {
                this.writeBackup = true;
            }
            if (opts.isSet("quotestr")) {
                this.quoteStr = opts.get("quotestr");
            }
            if (opts.isSet("tabsize")) {
                this.tabs = opts.getNumber("tabsize");
            }
            if (opts.isSet("quickblank")) {
                this.quickBlank = true;
            }
            if (opts.isSet("constantshow")) {
                this.constantCursor = true;
            }
            if (opts.isSet("emptyline")) {
                this.oneMoreLine = false;
            }
            if (opts.isSet("jumpyscrolling")) {
                this.smoothScrolling = false;
            }
            if (opts.isSet("linenumbers")) {
                this.printLineNumbers = true;
            }
            if (opts.isSet("mouse")) {
                this.mouseSupport = true;
            }
            if (opts.isSet("softwrap")) {
                this.wrapping = true;
            }
            if (opts.isSet("atblanks")) {
                this.atBlanks = true;
            }
            if (opts.isSet("suspend")) {
                this.enableSuspension();
            }
            if (opts.isSet("view")) {
                this.view = true;
            }
            if (opts.isSet("cutfromcursor")) {
                this.cut2end = true;
            }
            if (opts.isSet("tempfile")) {
                this.tempFile = true;
            }
            if (opts.isSet("historylog")) {
                this.historyLog = opts.get("historyLog");
            }
            if (opts.isSet("tabstospaces")) {
                this.tabsToSpaces = true;
            }
            if (opts.isSet("autoindent")) {
                this.autoIndent = true;
            }
        }
        this.bindKeys();
        if (configPath != null && this.historyLog != null) {
            try {
                this.patternHistory = new PatternHistory(configPath.getUserConfig(this.historyLog, true));
            }
            catch (final IOException e) {
                this.errorMessage = "Encountered error while reading pattern-history file: " + this.historyLog;
            }
        }
    }
    
    private void parseConfig(final Path file) throws IOException {
        try (final BufferedReader reader = Files.newBufferedReader(file)) {
            String line;
            while ((line = reader.readLine()) != null) {
                line = line.trim();
                if (!line.isEmpty() && !line.startsWith("#")) {
                    final List<String> parts = SyntaxHighlighter.RuleSplitter.split(line);
                    if (parts.get(0).equals("include")) {
                        SyntaxHighlighter.nanorcInclude(file, parts.get(1), this.syntaxFiles);
                    }
                    else if (parts.get(0).equals("theme")) {
                        SyntaxHighlighter.nanorcTheme(file, parts.get(1), this.syntaxFiles);
                    }
                    else if (parts.size() == 2 && (parts.get(0).equals("set") || parts.get(0).equals("unset"))) {
                        final String option = parts.get(1);
                        final boolean val = parts.get(0).equals("set");
                        final String s = option;
                        switch (s) {
                            case "linenumbers": {
                                this.printLineNumbers = val;
                                continue;
                            }
                            case "jumpyscrolling": {
                                this.smoothScrolling = !val;
                                continue;
                            }
                            case "smooth": {
                                this.smoothScrolling = val;
                                continue;
                            }
                            case "softwrap": {
                                this.wrapping = val;
                                continue;
                            }
                            case "mouse": {
                                this.mouseSupport = val;
                                continue;
                            }
                            case "emptyline": {
                                this.oneMoreLine = val;
                                continue;
                            }
                            case "morespace": {
                                this.oneMoreLine = !val;
                                continue;
                            }
                            case "constantshow": {
                                this.constantCursor = val;
                                continue;
                            }
                            case "quickblank": {
                                this.quickBlank = val;
                                continue;
                            }
                            case "atblanks": {
                                this.atBlanks = val;
                                continue;
                            }
                            case "suspend": {
                                this.enableSuspension();
                                continue;
                            }
                            case "view": {
                                this.view = val;
                                continue;
                            }
                            case "cutfromcursor": {
                                this.cut2end = val;
                                continue;
                            }
                            case "tempfile": {
                                this.tempFile = val;
                                continue;
                            }
                            case "tabstospaces": {
                                this.tabsToSpaces = val;
                                continue;
                            }
                            case "autoindent": {
                                this.autoIndent = val;
                                continue;
                            }
                            default: {
                                this.errorMessage = "Nano config: Unknown or unsupported configuration option " + option;
                                continue;
                            }
                        }
                    }
                    else if (parts.size() == 3 && parts.get(0).equals("set")) {
                        final String option = parts.get(1);
                        final String val2 = parts.get(2);
                        final String s2 = option;
                        switch (s2) {
                            case "quotestr": {
                                this.quoteStr = val2;
                                continue;
                            }
                            case "punct": {
                                this.punct = val2;
                                continue;
                            }
                            case "matchbrackets": {
                                this.matchBrackets = val2;
                                continue;
                            }
                            case "brackets": {
                                this.brackets = val2;
                                continue;
                            }
                            case "historylog": {
                                this.historyLog = val2;
                                continue;
                            }
                            default: {
                                this.errorMessage = "Nano config: Unknown or unsupported configuration option " + option;
                                continue;
                            }
                        }
                    }
                    else if (parts.get(0).equals("bind") || parts.get(0).equals("unbind")) {
                        this.errorMessage = "Nano config: Key bindings can not be changed!";
                    }
                    else {
                        this.errorMessage = "Nano config: Bad configuration '" + line + "'";
                    }
                }
            }
        }
    }
    
    @Override
    public void setRestricted(final boolean restricted) {
        this.restricted = restricted;
    }
    
    public void open(final String... files) throws IOException {
        this.open(Arrays.asList(files));
    }
    
    @Override
    public void open(final List<String> files) throws IOException {
        for (String file : files) {
            file = (file.startsWith("~") ? file.replace("~", System.getProperty("user.home")) : file);
            if (file.contains("*") || file.contains("?")) {
                for (final Path p : Commands.findFiles(this.root, file)) {
                    this.buffers.add(new Buffer(p.toString()));
                }
            }
            else {
                this.buffers.add(new Buffer(file));
            }
        }
    }
    
    @Override
    public void run() throws IOException {
        if (this.buffers.isEmpty()) {
            this.buffers.add(new Buffer(null));
        }
        this.buffer = this.buffers.get(this.bufferIndex);
        final Attributes attributes = this.terminal.getAttributes();
        final Attributes newAttr = new Attributes(attributes);
        if (this.vsusp > 0) {
            attributes.setControlChar(Attributes.ControlChar.VSUSP, this.vsusp);
        }
        newAttr.setLocalFlags(EnumSet.of(Attributes.LocalFlag.ICANON, Attributes.LocalFlag.ECHO, Attributes.LocalFlag.IEXTEN, Attributes.LocalFlag.ISIG), false);
        newAttr.setInputFlags(EnumSet.of(Attributes.InputFlag.IXON, Attributes.InputFlag.ICRNL, Attributes.InputFlag.INLCR), false);
        newAttr.setControlChar(Attributes.ControlChar.VMIN, 1);
        newAttr.setControlChar(Attributes.ControlChar.VTIME, 0);
        newAttr.setControlChar(Attributes.ControlChar.VINTR, 0);
        this.terminal.setAttributes(newAttr);
        this.terminal.puts(InfoCmp.Capability.enter_ca_mode, new Object[0]);
        this.terminal.puts(InfoCmp.Capability.keypad_xmit, new Object[0]);
        if (this.mouseSupport) {
            this.mouseTracking = this.terminal.getCurrentMouseTracking();
            this.terminal.trackMouse(Terminal.MouseTracking.Any);
        }
        this.shortcuts = this.standardShortcuts();
        Terminal.SignalHandler prevHandler = null;
        final Status status = Status.getStatus(this.terminal, false);
        try {
            this.size.copy(this.terminal.getSize());
            if (status != null) {
                status.suspend();
            }
            this.buffer.open();
            if (this.errorMessage != null) {
                this.setMessage(this.errorMessage);
                this.errorMessage = null;
            }
            else if (this.buffer.file != null) {
                this.setMessage("Read " + this.buffer.lines.size() + " lines");
            }
            this.display.clear();
            this.display.reset();
            this.display.resize(this.size.getRows(), this.size.getColumns());
            prevHandler = this.terminal.handle(Terminal.Signal.WINCH, this::handle);
            this.display();
        Block_10:
            while (true) {
                final Operation op;
                switch ((op = this.readOperation(this.keys)).ordinal()) {
                    case 1: {
                        if (this.help) {
                            this.resetSuggestion();
                            break;
                        }
                        if (this.quit()) {
                            break Block_10;
                        }
                        break;
                    }
                    case 2: {
                        this.write();
                        break;
                    }
                    case 3: {
                        this.read();
                        break;
                    }
                    case 12: {
                        if (this.help && this.suggestionBox != null) {
                            this.suggestionBox.up();
                            break;
                        }
                        this.buffer.moveUp(1);
                        break;
                    }
                    case 13: {
                        if (this.help && this.suggestionBox != null) {
                            this.suggestionBox.down();
                            break;
                        }
                        this.buffer.moveDown(1);
                        break;
                    }
                    case 14: {
                        this.buffer.moveLeft(1);
                        if (this.help) {
                            this.resetSuggestion();
                            break;
                        }
                        break;
                    }
                    case 15: {
                        this.buffer.moveRight(1);
                        if (this.help) {
                            this.resetSuggestion();
                            break;
                        }
                        break;
                    }
                    case 16: {
                        if (this.help) {
                            this.insertHelp = true;
                            break;
                        }
                        this.buffer.insert(this.bindingReader.getLastBinding());
                        break;
                    }
                    case 17: {
                        this.buffer.backspace(1);
                        break;
                    }
                    case 54: {
                        this.buffer.delete(1);
                        break;
                    }
                    case 6: {
                        this.wrap();
                        break;
                    }
                    case 7: {
                        this.numbers();
                        break;
                    }
                    case 8: {
                        this.smoothScrolling();
                        break;
                    }
                    case 9: {
                        this.mouseSupport();
                        break;
                    }
                    case 10: {
                        this.oneMoreLine();
                        break;
                    }
                    case 11: {
                        this.clearScreen();
                        break;
                    }
                    case 19: {
                        this.prevBuffer();
                        break;
                    }
                    case 18: {
                        this.nextBuffer();
                        break;
                    }
                    case 32: {
                        this.curPos();
                        break;
                    }
                    case 27: {
                        this.help = true;
                        break;
                    }
                    case 28: {
                        this.buffer.beginningOfLine();
                        break;
                    }
                    case 29: {
                        this.buffer.endOfLine();
                        break;
                    }
                    case 30: {
                        this.buffer.firstLine();
                        break;
                    }
                    case 31: {
                        this.buffer.lastLine();
                        break;
                    }
                    case 22: {
                        this.buffer.prevPage();
                        break;
                    }
                    case 21: {
                        this.buffer.nextPage();
                        break;
                    }
                    case 23: {
                        this.buffer.scrollUp(1);
                        break;
                    }
                    case 24: {
                        this.buffer.scrollDown(1);
                        break;
                    }
                    case 38: {
                        this.searchToReplace = false;
                        this.searchAndReplace();
                        break;
                    }
                    case 58: {
                        this.searchToReplace = true;
                        this.searchAndReplace();
                        break;
                    }
                    case 51: {
                        this.buffer.nextSearch();
                        break;
                    }
                    case 20: {
                        this.help("nano-main-help.txt");
                        break;
                    }
                    case 68: {
                        this.constantCursor();
                        break;
                    }
                    case 53: {
                        this.buffer.insert(new String(Character.toChars(this.bindingReader.readCharacter())));
                        break;
                    }
                    case 52: {
                        this.buffer.matching();
                        break;
                    }
                    case 76: {
                        this.mouseEvent();
                        break;
                    }
                    case 77: {
                        this.toggleSuspension();
                        break;
                    }
                    case 60: {
                        this.buffer.copy();
                        break;
                    }
                    case 57: {
                        this.buffer.cut();
                        break;
                    }
                    case 75: {
                        this.buffer.uncut();
                        break;
                    }
                    case 4: {
                        this.gotoLine();
                        this.curPos();
                        break;
                    }
                    case 73: {
                        this.cut2end = !this.cut2end;
                        this.setMessage("Cut to end " + (this.cut2end ? "enabled" : "disabled"));
                        break;
                    }
                    case 65: {
                        this.buffer.cut(true);
                        break;
                    }
                    case 59: {
                        this.mark = !this.mark;
                        this.setMessage("Mark " + (this.mark ? "Set" : "Unset"));
                        this.buffer.mark();
                        break;
                    }
                    case 70: {
                        this.highlight = !this.highlight;
                        this.setMessage("Highlight " + (this.highlight ? "enabled" : "disabled"));
                        break;
                    }
                    case 74: {
                        this.tabsToSpaces = !this.tabsToSpaces;
                        this.setMessage("Conversion of typed tabs to spaces " + (this.tabsToSpaces ? "enabled" : "disabled"));
                        break;
                    }
                    case 72: {
                        this.autoIndent = !this.autoIndent;
                        this.setMessage("Auto indent " + (this.autoIndent ? "enabled" : "disabled"));
                        break;
                    }
                    default: {
                        this.setMessage("Unsupported " + op.name().toLowerCase().replace('_', '-'));
                        break;
                    }
                }
                this.display();
            }
        }
        finally {
            if (this.mouseSupport) {
                this.terminal.trackMouse(this.mouseTracking);
            }
            if (!this.terminal.puts(InfoCmp.Capability.exit_ca_mode, new Object[0])) {
                this.terminal.puts(InfoCmp.Capability.clear_screen, new Object[0]);
            }
            this.terminal.puts(InfoCmp.Capability.keypad_local, new Object[0]);
            this.terminal.flush();
            this.terminal.setAttributes(attributes);
            this.terminal.handle(Terminal.Signal.WINCH, prevHandler);
            if (status != null) {
                status.restore();
            }
            this.patternHistory.persist();
        }
    }
    
    private void resetSuggestion() {
        this.suggestions = null;
        this.suggestionBox = null;
        this.insertHelp = false;
        this.help = false;
    }
    
    private int editInputBuffer(final Operation operation, int curPos) {
        switch (operation.ordinal()) {
            case 16: {
                this.editBuffer.insert(curPos++, this.bindingReader.getLastBinding());
                break;
            }
            case 17: {
                if (curPos > 0) {
                    this.editBuffer.deleteCharAt(--curPos);
                    break;
                }
                break;
            }
            case 14: {
                if (curPos > 0) {
                    --curPos;
                    break;
                }
                break;
            }
            case 15: {
                if (curPos < this.editBuffer.length()) {
                    ++curPos;
                    break;
                }
                break;
            }
        }
        return curPos;
    }
    
    boolean write() throws IOException {
        final KeyMap<Operation> writeKeyMap = new KeyMap<Operation>();
        if (!this.restricted) {
            writeKeyMap.setUnicode(Operation.INSERT);
            for (char i = ' '; i < '\u0100'; ++i) {
                writeKeyMap.bind(Operation.INSERT, Character.toString(i));
            }
            for (char i = 'A'; i <= 'Z'; ++i) {
                writeKeyMap.bind(Operation.DO_LOWER_CASE, KeyMap.alt(i));
            }
            writeKeyMap.bind(Operation.BACKSPACE, KeyMap.del());
            writeKeyMap.bind(Operation.APPEND_MODE, KeyMap.alt('a'));
            writeKeyMap.bind(Operation.PREPEND_MODE, KeyMap.alt('p'));
            writeKeyMap.bind(Operation.BACKUP, KeyMap.alt('b'));
            writeKeyMap.bind(Operation.TO_FILES, KeyMap.ctrl('T'));
        }
        writeKeyMap.bind(Operation.MAC_FORMAT, KeyMap.alt('m'));
        writeKeyMap.bind(Operation.DOS_FORMAT, KeyMap.alt('d'));
        writeKeyMap.bind(Operation.ACCEPT, "\r");
        writeKeyMap.bind(Operation.CANCEL, KeyMap.ctrl('C'));
        writeKeyMap.bind(Operation.HELP, KeyMap.ctrl('G'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f1));
        writeKeyMap.bind(Operation.MOUSE_EVENT, (CharSequence[])MouseSupport.keys(this.terminal));
        writeKeyMap.bind(Operation.TOGGLE_SUSPENSION, KeyMap.alt('z'));
        writeKeyMap.bind(Operation.RIGHT, KeyMap.key(this.terminal, InfoCmp.Capability.key_right));
        writeKeyMap.bind(Operation.LEFT, KeyMap.key(this.terminal, InfoCmp.Capability.key_left));
        this.editMessage = this.getWriteMessage();
        this.editBuffer.setLength();
        this.editBuffer.append((this.buffer.file == null) ? "" : this.buffer.file);
        int curPos = this.editBuffer.length();
        this.shortcuts = this.writeShortcuts();
        this.display(curPos);
        while (true) {
            final Operation op = this.readOperation(writeKeyMap);
            switch (op.ordinal()) {
                case 37: {
                    this.editMessage = null;
                    this.shortcuts = this.standardShortcuts();
                    return false;
                }
                case 36: {
                    this.editMessage = null;
                    if (this.save(this.editBuffer.toString())) {
                        this.shortcuts = this.standardShortcuts();
                        return true;
                    }
                    return false;
                }
                case 20: {
                    this.help("nano-write-help.txt");
                    break;
                }
                case 40: {
                    this.buffer.format = ((this.buffer.format == WriteFormat.MAC) ? WriteFormat.UNIX : WriteFormat.MAC);
                    break;
                }
                case 41: {
                    this.buffer.format = ((this.buffer.format == WriteFormat.DOS) ? WriteFormat.UNIX : WriteFormat.DOS);
                    break;
                }
                case 42: {
                    this.writeMode = ((this.writeMode == WriteMode.APPEND) ? WriteMode.WRITE : WriteMode.APPEND);
                    break;
                }
                case 43: {
                    this.writeMode = ((this.writeMode == WriteMode.PREPEND) ? WriteMode.WRITE : WriteMode.PREPEND);
                    break;
                }
                case 44: {
                    this.writeBackup = !this.writeBackup;
                    break;
                }
                case 76: {
                    this.mouseEvent();
                    break;
                }
                case 77: {
                    this.toggleSuspension();
                    break;
                }
                default: {
                    curPos = this.editInputBuffer(op, curPos);
                    break;
                }
            }
            this.editMessage = this.getWriteMessage();
            this.display(curPos);
        }
    }
    
    private Operation readOperation(final KeyMap<Operation> keymap) {
        Operation op;
        while (true) {
            op = this.bindingReader.readBinding(keymap);
            if (op != Operation.DO_LOWER_CASE) {
                break;
            }
            this.bindingReader.runMacro(this.bindingReader.getLastBinding().toLowerCase());
        }
        return op;
    }
    
    private boolean save(final String name) throws IOException {
        final Path orgPath = (this.buffer.file != null) ? this.root.resolve(this.buffer.file) : null;
        final Path newPath = this.root.resolve(name);
        final boolean isSame = orgPath != null && Files.exists(orgPath, new LinkOption[0]) && Files.exists(newPath, new LinkOption[0]) && Files.isSameFile(orgPath, newPath);
        if (!isSame && Files.exists(Paths.get(name, new String[0]), new LinkOption[0]) && this.writeMode == WriteMode.WRITE) {
            final Operation op = this.getYNC("File exists, OVERWRITE ? ");
            if (op != Operation.YES) {
                return false;
            }
        }
        else if (!Files.exists(newPath, new LinkOption[0])) {
            Files.createFile(newPath, (FileAttribute<?>[])new FileAttribute[0]);
        }
        final Path t = Files.createTempFile("jline-", ".temp", (FileAttribute<?>[])new FileAttribute[0]);
        try (final OutputStream os = Files.newOutputStream(t, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.CREATE)) {
            if (this.writeMode == WriteMode.APPEND && Files.isReadable(newPath)) {
                Files.copy(newPath, os);
            }
            final Writer w = new OutputStreamWriter(os, this.buffer.charset);
            for (int i = 0; i < this.buffer.lines.size(); ++i) {
                w.write(this.buffer.lines.get(i));
                switch (this.buffer.format.ordinal()) {
                    case 0: {
                        w.write("\n");
                        break;
                    }
                    case 1: {
                        w.write("\r\n");
                        break;
                    }
                    case 2: {
                        w.write("\r");
                        break;
                    }
                }
            }
            w.flush();
            if (this.writeMode == WriteMode.PREPEND && Files.isReadable(newPath)) {
                Files.copy(newPath, os);
            }
            if (this.writeBackup) {
                Files.move(newPath, newPath.resolveSibling(newPath.getFileName().toString() + "~"), StandardCopyOption.REPLACE_EXISTING);
            }
            Files.move(t, newPath, StandardCopyOption.REPLACE_EXISTING);
            if (this.writeMode == WriteMode.WRITE) {
                this.buffer.file = name;
                this.buffer.dirty = false;
            }
            this.setMessage("Wrote " + this.buffer.lines.size() + " lines");
            return true;
        }
        catch (final IOException e) {
            this.setMessage("Error writing " + name + ": " + e);
            return false;
        }
        finally {
            Files.deleteIfExists(t);
            this.writeMode = WriteMode.WRITE;
        }
    }
    
    private Operation getYNC(final String message) {
        return this.getYNC(message, false);
    }
    
    private Operation getYNC(final String message, final boolean andAll) {
        final String oldEditMessage = this.editMessage;
        final String oldEditBuffer = this.editBuffer.toString();
        final LinkedHashMap<String, String> oldShortcuts = this.shortcuts;
        try {
            this.editMessage = message;
            this.editBuffer.setLength();
            final KeyMap<Operation> yncKeyMap = new KeyMap<Operation>();
            yncKeyMap.bind(Operation.YES, "y", "Y");
            if (andAll) {
                yncKeyMap.bind(Operation.ALL, "a", "A");
            }
            yncKeyMap.bind(Operation.NO, "n", "N");
            yncKeyMap.bind(Operation.CANCEL, KeyMap.ctrl('C'));
            (this.shortcuts = new LinkedHashMap<String, String>()).put(" Y", "Yes");
            if (andAll) {
                this.shortcuts.put(" A", "All");
            }
            this.shortcuts.put(" N", "No");
            this.shortcuts.put("^C", "Cancel");
            this.display();
            return this.readOperation(yncKeyMap);
        }
        finally {
            this.editMessage = oldEditMessage;
            this.editBuffer.append(oldEditBuffer);
            this.shortcuts = oldShortcuts;
        }
    }
    
    private String getWriteMessage() {
        final StringBuilder sb = new StringBuilder();
        sb.append("File Name to ");
        switch (this.writeMode.ordinal()) {
            case 0: {
                sb.append("Write");
                break;
            }
            case 1: {
                sb.append("Append");
                break;
            }
            case 2: {
                sb.append("Prepend");
                break;
            }
        }
        switch (this.buffer.format.ordinal()) {
            case 1: {
                sb.append(" [DOS Format]");
                break;
            }
            case 2: {
                sb.append(" [Mac Format]");
                break;
            }
        }
        if (this.writeBackup) {
            sb.append(" [Backup]");
        }
        sb.append(": ");
        return sb.toString();
    }
    
    void read() {
        final KeyMap<Operation> readKeyMap = new KeyMap<Operation>();
        readKeyMap.setUnicode(Operation.INSERT);
        for (char i = ' '; i < '\u0100'; ++i) {
            readKeyMap.bind(Operation.INSERT, Character.toString(i));
        }
        for (char i = 'A'; i <= 'Z'; ++i) {
            readKeyMap.bind(Operation.DO_LOWER_CASE, KeyMap.alt(i));
        }
        readKeyMap.bind(Operation.BACKSPACE, KeyMap.del());
        readKeyMap.bind(Operation.NEW_BUFFER, KeyMap.alt('f'));
        readKeyMap.bind(Operation.TO_FILES, KeyMap.ctrl('T'));
        readKeyMap.bind(Operation.EXECUTE, KeyMap.ctrl('X'));
        readKeyMap.bind(Operation.ACCEPT, "\r");
        readKeyMap.bind(Operation.CANCEL, KeyMap.ctrl('C'));
        readKeyMap.bind(Operation.HELP, KeyMap.ctrl('G'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f1));
        readKeyMap.bind(Operation.MOUSE_EVENT, (CharSequence[])MouseSupport.keys(this.terminal));
        readKeyMap.bind(Operation.RIGHT, KeyMap.key(this.terminal, InfoCmp.Capability.key_right));
        readKeyMap.bind(Operation.LEFT, KeyMap.key(this.terminal, InfoCmp.Capability.key_left));
        this.editMessage = this.getReadMessage();
        this.editBuffer.setLength();
        int curPos = this.editBuffer.length();
        this.shortcuts = this.readShortcuts();
        this.display(curPos);
        while (true) {
            final Operation op = this.readOperation(readKeyMap);
            switch (op.ordinal()) {
                case 37: {
                    this.editMessage = null;
                    this.shortcuts = this.standardShortcuts();
                    return;
                }
                case 36: {
                    this.editMessage = null;
                    final String file = this.editBuffer.toString();
                    final boolean empty = file.isEmpty();
                    final Path p = empty ? null : this.root.resolve(file);
                    if (!this.readNewBuffer && !empty && !Files.exists(p, new LinkOption[0])) {
                        this.setMessage("\"" + file + "\" not found");
                    }
                    else if (!empty && Files.isDirectory(p, new LinkOption[0])) {
                        this.setMessage("\"" + file + "\" is a directory");
                    }
                    else if (!empty && !Files.isRegularFile(p, new LinkOption[0])) {
                        this.setMessage("\"" + file + "\" is not a regular file");
                    }
                    else {
                        final Buffer buf = new Buffer(empty ? null : file);
                        try {
                            buf.open();
                            if (this.readNewBuffer) {
                                this.buffers.add(++this.bufferIndex, buf);
                                this.buffer = buf;
                            }
                            else {
                                this.buffer.insert(String.join("\n", buf.lines));
                            }
                            this.setMessage(null);
                        }
                        catch (final IOException e) {
                            this.setMessage("Error reading " + file + ": " + e.getMessage());
                        }
                    }
                    this.shortcuts = this.standardShortcuts();
                    return;
                }
                case 20: {
                    this.help("nano-read-help.txt");
                    break;
                }
                case 49: {
                    this.readNewBuffer = !this.readNewBuffer;
                    break;
                }
                case 76: {
                    this.mouseEvent();
                    break;
                }
                default: {
                    curPos = this.editInputBuffer(op, curPos);
                    break;
                }
            }
            this.editMessage = this.getReadMessage();
            this.display(curPos);
        }
    }
    
    private String getReadMessage() {
        final StringBuilder sb = new StringBuilder();
        sb.append("File to insert");
        if (this.readNewBuffer) {
            sb.append(" into new buffer");
        }
        sb.append(" [from ./]: ");
        return sb.toString();
    }
    
    void gotoLine() {
        final KeyMap<Operation> readKeyMap = new KeyMap<Operation>();
        readKeyMap.setUnicode(Operation.INSERT);
        for (char i = ' '; i < '\u0100'; ++i) {
            readKeyMap.bind(Operation.INSERT, Character.toString(i));
        }
        readKeyMap.bind(Operation.BACKSPACE, KeyMap.del());
        readKeyMap.bind(Operation.ACCEPT, "\r");
        readKeyMap.bind(Operation.HELP, KeyMap.ctrl('G'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f1));
        readKeyMap.bind(Operation.CANCEL, KeyMap.ctrl('C'));
        readKeyMap.bind(Operation.MOUSE_EVENT, (CharSequence[])MouseSupport.keys(this.terminal));
        readKeyMap.bind(Operation.RIGHT, KeyMap.key(this.terminal, InfoCmp.Capability.key_right));
        readKeyMap.bind(Operation.LEFT, KeyMap.key(this.terminal, InfoCmp.Capability.key_left));
        readKeyMap.bind(Operation.FIRST_LINE, KeyMap.ctrl('Y'));
        readKeyMap.bind(Operation.LAST_LINE, KeyMap.ctrl('V'));
        readKeyMap.bind(Operation.SEARCH, KeyMap.ctrl('T'));
        this.editMessage = "Enter line number, column number: ";
        this.editBuffer.setLength();
        int curPos = this.editBuffer.length();
        this.shortcuts = this.gotoShortcuts();
        this.display(curPos);
        while (true) {
            final Operation op = this.readOperation(readKeyMap);
            switch (op.ordinal()) {
                case 37: {
                    this.editMessage = null;
                    this.shortcuts = this.standardShortcuts();
                    return;
                }
                case 30: {
                    this.editMessage = null;
                    this.buffer.firstLine();
                    this.shortcuts = this.standardShortcuts();
                    return;
                }
                case 31: {
                    this.editMessage = null;
                    this.buffer.lastLine();
                    this.shortcuts = this.standardShortcuts();
                    return;
                }
                case 38: {
                    this.searchToReplace = false;
                    this.searchAndReplace();
                    return;
                }
                case 36: {
                    this.editMessage = null;
                    final String[] pos = this.editBuffer.toString().split(",", 2);
                    final int[] args = { 0, 0 };
                    try {
                        for (int j = 0; j < pos.length; ++j) {
                            if (!pos[j].trim().isEmpty()) {
                                args[j] = Integer.parseInt(pos[j]) - 1;
                                if (args[j] < 0) {
                                    throw new NumberFormatException();
                                }
                            }
                        }
                        this.buffer.gotoLine(args[1], args[0]);
                    }
                    catch (final NumberFormatException ex) {
                        this.setMessage("Invalid line or column number");
                    }
                    catch (final Exception ex2) {
                        this.setMessage("Internal error: " + ex2.getMessage());
                    }
                    this.shortcuts = this.standardShortcuts();
                    return;
                }
                case 20: {
                    this.help("nano-goto-help.txt");
                    break;
                }
                default: {
                    curPos = this.editInputBuffer(op, curPos);
                    break;
                }
            }
            this.display(curPos);
        }
    }
    
    private LinkedHashMap<String, String> gotoShortcuts() {
        final LinkedHashMap<String, String> shortcuts = new LinkedHashMap<String, String>();
        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<String, String> readShortcuts() {
        final LinkedHashMap<String, String> shortcuts = new LinkedHashMap<String, String>();
        shortcuts.put("^G", "Get Help");
        shortcuts.put("^T", "To Files");
        shortcuts.put("M-F", "New Buffer");
        shortcuts.put("^C", "Cancel");
        shortcuts.put("^X", "Execute Command");
        return shortcuts;
    }
    
    private LinkedHashMap<String, String> writeShortcuts() {
        final LinkedHashMap<String, String> s = new LinkedHashMap<String, String>();
        s.put("^G", "Get Help");
        s.put("M-M", "Mac Format");
        s.put("^C", "Cancel");
        s.put("M-D", "DOS Format");
        if (!this.restricted) {
            s.put("^T", "To Files");
            s.put("M-P", "Prepend");
            s.put("M-A", "Append");
            s.put("M-B", "Backup File");
        }
        return s;
    }
    
    private LinkedHashMap<String, String> helpShortcuts() {
        final LinkedHashMap<String, String> s = new LinkedHashMap<String, String>();
        s.put("^L", "Refresh");
        s.put("^Y", "Prev Page");
        s.put("^P", "Prev Line");
        s.put("M-\\", "First Line");
        s.put("^X", "Exit");
        s.put("^V", "Next Page");
        s.put("^N", "Next Line");
        s.put("M-/", "Last Line");
        return s;
    }
    
    private LinkedHashMap<String, String> searchShortcuts() {
        final LinkedHashMap<String, String> s = new LinkedHashMap<String, String>();
        s.put("^G", "Get Help");
        s.put("^Y", "First Line");
        if (this.searchToReplace) {
            s.put("^R", "No Replace");
        }
        else {
            s.put("^R", "Replace");
            s.put("^W", "Beg of Par");
        }
        s.put("M-C", "Case Sens");
        s.put("M-R", "Regexp");
        s.put("^C", "Cancel");
        s.put("^V", "Last Line");
        s.put("^T", "Go To Line");
        if (!this.searchToReplace) {
            s.put("^O", "End of Par");
        }
        s.put("M-B", "Backwards");
        s.put("^P", "PrevHstory");
        return s;
    }
    
    private LinkedHashMap<String, String> replaceShortcuts() {
        final LinkedHashMap<String, String> s = new LinkedHashMap<String, String>();
        s.put("^G", "Get Help");
        s.put("^Y", "First Line");
        s.put("^P", "PrevHstory");
        s.put("^C", "Cancel");
        s.put("^V", "Last Line");
        s.put("^N", "NextHstory");
        return s;
    }
    
    private LinkedHashMap<String, String> standardShortcuts() {
        final LinkedHashMap<String, String> s = new LinkedHashMap<String, String>();
        s.put("^G", "Get Help");
        if (!this.view) {
            s.put("^O", "WriteOut");
        }
        s.put("^R", "Read File");
        s.put("^Y", "Prev Page");
        if (!this.view) {
            s.put("^K", "Cut Text");
        }
        s.put("^C", "Cur Pos");
        s.put("^X", "Exit");
        if (!this.view) {
            s.put("^J", "Justify");
        }
        s.put("^W", "Where Is");
        s.put("^V", "Next Page");
        if (!this.view) {
            s.put("^U", "UnCut Text");
        }
        s.put("^T", "To Spell");
        return s;
    }
    
    void help(final String help) {
        final Buffer org = this.buffer;
        final Buffer newBuf = new Buffer(null);
        try (final InputStream is = this.getClass().getResourceAsStream(help)) {
            newBuf.open(is);
        }
        catch (final IOException e) {
            this.setMessage("Unable to read help");
            return;
        }
        final LinkedHashMap<String, String> oldShortcuts = this.shortcuts;
        this.shortcuts = this.helpShortcuts();
        final boolean oldWrapping = this.wrapping;
        final boolean oldPrintLineNumbers = this.printLineNumbers;
        final boolean oldConstantCursor = this.constantCursor;
        final boolean oldAtBlanks = this.atBlanks;
        final boolean oldHighlight = this.highlight;
        final String oldEditMessage = this.editMessage;
        this.editMessage = "";
        this.wrapping = true;
        this.atBlanks = true;
        this.printLineNumbers = false;
        this.constantCursor = false;
        this.highlight = false;
        this.buffer = newBuf;
        if (!oldWrapping) {
            this.buffer.computeAllOffsets();
        }
        try {
            this.message = null;
            this.terminal.puts(InfoCmp.Capability.cursor_invisible, new Object[0]);
            this.display();
        Label_0312:
            while (true) {
                switch (this.readOperation(this.keys).ordinal()) {
                    case 1: {
                        break Label_0312;
                    }
                    case 30: {
                        this.buffer.firstLine();
                        break;
                    }
                    case 31: {
                        this.buffer.lastLine();
                        break;
                    }
                    case 22: {
                        this.buffer.prevPage();
                        break;
                    }
                    case 21: {
                        this.buffer.nextPage();
                        break;
                    }
                    case 12: {
                        this.buffer.scrollUp(1);
                        break;
                    }
                    case 13: {
                        this.buffer.scrollDown(1);
                        break;
                    }
                    case 11: {
                        this.clearScreen();
                        break;
                    }
                    case 76: {
                        this.mouseEvent();
                        break;
                    }
                    case 77: {
                        this.toggleSuspension();
                        break;
                    }
                }
                this.display();
            }
        }
        finally {
            this.buffer = org;
            this.wrapping = oldWrapping;
            this.printLineNumbers = oldPrintLineNumbers;
            this.constantCursor = oldConstantCursor;
            this.shortcuts = oldShortcuts;
            this.atBlanks = oldAtBlanks;
            this.highlight = oldHighlight;
            this.editMessage = oldEditMessage;
            this.terminal.puts(InfoCmp.Capability.cursor_visible, new Object[0]);
            if (!oldWrapping) {
                this.buffer.computeAllOffsets();
            }
        }
    }
    
    void searchAndReplace() {
        try {
            this.search();
            if (!this.searchToReplace) {
                return;
            }
            final String replaceTerm = this.replace();
            int replaced = 0;
            boolean all = false;
            boolean found = true;
            final List<Integer> matches = new ArrayList<Integer>();
            Operation op = Operation.NO;
            while (found) {
                found = this.buffer.nextSearch();
                if (found) {
                    final int[] re = this.buffer.highlightStart();
                    final int col = this.searchBackwards ? (this.buffer.length(this.buffer.getLine(re[0])) - re[1]) : re[1];
                    final int match = re[0] * 10000 + col;
                    if (matches.contains(match)) {
                        break;
                    }
                    matches.add(match);
                    if (!all) {
                        op = this.getYNC("Replace this instance? ", true);
                    }
                }
                else {
                    op = Operation.NO;
                }
                switch (op.ordinal()) {
                    case 48: {
                        all = true;
                        this.buffer.replaceFromCursor(this.matchedLength, replaceTerm);
                        ++replaced;
                        continue;
                    }
                    case 46: {
                        this.buffer.replaceFromCursor(this.matchedLength, replaceTerm);
                        ++replaced;
                        continue;
                    }
                    case 37: {
                        found = false;
                        continue;
                    }
                    default: {
                        continue;
                    }
                }
            }
            this.message = "Replaced " + replaced + " occurrences";
        }
        catch (final Exception ex) {}
        finally {
            this.searchToReplace = false;
            this.matchedLength = -1;
            this.shortcuts = this.standardShortcuts();
            this.editMessage = null;
        }
    }
    
    void search() throws IOException {
        final KeyMap<Operation> searchKeyMap = new KeyMap<Operation>();
        searchKeyMap.setUnicode(Operation.INSERT);
        for (char i = ' '; i < '\u0100'; ++i) {
            searchKeyMap.bind(Operation.INSERT, Character.toString(i));
        }
        for (char i = 'A'; i <= 'Z'; ++i) {
            searchKeyMap.bind(Operation.DO_LOWER_CASE, KeyMap.alt(i));
        }
        searchKeyMap.bind(Operation.BACKSPACE, KeyMap.del());
        searchKeyMap.bind(Operation.CASE_SENSITIVE, KeyMap.alt('c'));
        searchKeyMap.bind(Operation.BACKWARDS, KeyMap.alt('b'));
        searchKeyMap.bind(Operation.REGEXP, KeyMap.alt('r'));
        searchKeyMap.bind(Operation.ACCEPT, "\r");
        searchKeyMap.bind(Operation.CANCEL, KeyMap.ctrl('C'));
        searchKeyMap.bind(Operation.HELP, KeyMap.ctrl('G'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f1));
        searchKeyMap.bind(Operation.FIRST_LINE, KeyMap.ctrl('Y'));
        searchKeyMap.bind(Operation.LAST_LINE, KeyMap.ctrl('V'));
        searchKeyMap.bind(Operation.MOUSE_EVENT, (CharSequence[])MouseSupport.keys(this.terminal));
        searchKeyMap.bind(Operation.RIGHT, KeyMap.key(this.terminal, InfoCmp.Capability.key_right));
        searchKeyMap.bind(Operation.LEFT, KeyMap.key(this.terminal, InfoCmp.Capability.key_left));
        searchKeyMap.bind(Operation.UP, KeyMap.key(this.terminal, InfoCmp.Capability.key_up));
        searchKeyMap.bind(Operation.DOWN, KeyMap.key(this.terminal, InfoCmp.Capability.key_down));
        searchKeyMap.bind(Operation.TOGGLE_REPLACE, KeyMap.ctrl('R'));
        this.editMessage = this.getSearchMessage();
        this.editBuffer.setLength();
        String currentBuffer = this.editBuffer.toString();
        int curPos = this.editBuffer.length();
        this.shortcuts = this.searchShortcuts();
        this.display(curPos);
        try {
            while (true) {
                final Operation op = this.readOperation(searchKeyMap);
                switch (op.ordinal()) {
                    case 12: {
                        this.editBuffer.setLength();
                        this.editBuffer.append(this.patternHistory.up(currentBuffer));
                        curPos = this.editBuffer.length();
                        break;
                    }
                    case 13: {
                        this.editBuffer.setLength();
                        this.editBuffer.append(this.patternHistory.down(currentBuffer));
                        curPos = this.editBuffer.length();
                        break;
                    }
                    case 33: {
                        this.searchCaseSensitive = !this.searchCaseSensitive;
                        break;
                    }
                    case 34: {
                        this.searchBackwards = !this.searchBackwards;
                        break;
                    }
                    case 35: {
                        this.searchRegexp = !this.searchRegexp;
                        break;
                    }
                    case 37: {
                        throw new IllegalArgumentException();
                    }
                    case 36: {
                        if (this.editBuffer.length() > 0) {
                            this.searchTerm = this.editBuffer.toString();
                        }
                        if (this.searchTerm == null || this.searchTerm.isEmpty()) {
                            this.setMessage("Cancelled");
                            throw new IllegalArgumentException();
                        }
                        this.patternHistory.add(this.searchTerm);
                        if (!this.searchToReplace) {
                            this.buffer.nextSearch();
                        }
                        return;
                    }
                    case 20: {
                        if (this.searchToReplace) {
                            this.help("nano-search-replace-help.txt");
                            break;
                        }
                        this.help("nano-search-help.txt");
                        break;
                    }
                    case 30: {
                        this.buffer.firstLine();
                        break;
                    }
                    case 31: {
                        this.buffer.lastLine();
                        break;
                    }
                    case 76: {
                        this.mouseEvent();
                        break;
                    }
                    case 39: {
                        this.searchToReplace = !this.searchToReplace;
                        this.shortcuts = this.searchShortcuts();
                        break;
                    }
                    default: {
                        curPos = this.editInputBuffer(op, curPos);
                        currentBuffer = this.editBuffer.toString();
                        break;
                    }
                }
                this.editMessage = this.getSearchMessage();
                this.display(curPos);
            }
        }
        finally {
            this.shortcuts = this.standardShortcuts();
            this.editMessage = null;
        }
    }
    
    String replace() {
        final KeyMap<Operation> keyMap = new KeyMap<Operation>();
        keyMap.setUnicode(Operation.INSERT);
        for (char i = ' '; i < '\u0100'; ++i) {
            keyMap.bind(Operation.INSERT, Character.toString(i));
        }
        for (char i = 'A'; i <= 'Z'; ++i) {
            keyMap.bind(Operation.DO_LOWER_CASE, KeyMap.alt(i));
        }
        keyMap.bind(Operation.BACKSPACE, KeyMap.del());
        keyMap.bind(Operation.ACCEPT, "\r");
        keyMap.bind(Operation.CANCEL, KeyMap.ctrl('C'));
        keyMap.bind(Operation.HELP, KeyMap.ctrl('G'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f1));
        keyMap.bind(Operation.FIRST_LINE, KeyMap.ctrl('Y'));
        keyMap.bind(Operation.LAST_LINE, KeyMap.ctrl('V'));
        keyMap.bind(Operation.MOUSE_EVENT, (CharSequence[])MouseSupport.keys(this.terminal));
        keyMap.bind(Operation.RIGHT, KeyMap.key(this.terminal, InfoCmp.Capability.key_right));
        keyMap.bind(Operation.LEFT, KeyMap.key(this.terminal, InfoCmp.Capability.key_left));
        keyMap.bind(Operation.UP, KeyMap.key(this.terminal, InfoCmp.Capability.key_up));
        keyMap.bind(Operation.DOWN, KeyMap.key(this.terminal, InfoCmp.Capability.key_down));
        this.editMessage = "Replace with: ";
        this.editBuffer.setLength();
        String currentBuffer = this.editBuffer.toString();
        int curPos = this.editBuffer.length();
        this.shortcuts = this.replaceShortcuts();
        this.display(curPos);
        try {
            while (true) {
                final Operation op = this.readOperation(keyMap);
                switch (op.ordinal()) {
                    case 12: {
                        this.editBuffer.setLength();
                        this.editBuffer.append(this.patternHistory.up(currentBuffer));
                        curPos = this.editBuffer.length();
                        break;
                    }
                    case 13: {
                        this.editBuffer.setLength();
                        this.editBuffer.append(this.patternHistory.down(currentBuffer));
                        curPos = this.editBuffer.length();
                        break;
                    }
                    case 37: {
                        throw new IllegalArgumentException();
                    }
                    case 36: {
                        String replaceTerm = "";
                        if (this.editBuffer.length() > 0) {
                            replaceTerm = this.editBuffer.toString();
                        }
                        this.patternHistory.add(replaceTerm);
                        return replaceTerm;
                    }
                    case 20: {
                        this.help("nano-replace-help.txt");
                        break;
                    }
                    case 30: {
                        this.buffer.firstLine();
                        break;
                    }
                    case 31: {
                        this.buffer.lastLine();
                        break;
                    }
                    case 76: {
                        this.mouseEvent();
                        break;
                    }
                    default: {
                        curPos = this.editInputBuffer(op, curPos);
                        currentBuffer = this.editBuffer.toString();
                        break;
                    }
                }
                this.display(curPos);
            }
        }
        finally {
            this.shortcuts = this.standardShortcuts();
            this.editMessage = null;
        }
    }
    
    private String getSearchMessage() {
        final StringBuilder sb = new StringBuilder();
        sb.append("Search");
        if (this.searchToReplace) {
            sb.append(" (to replace)");
        }
        if (this.searchCaseSensitive) {
            sb.append(" [Case Sensitive]");
        }
        if (this.searchRegexp) {
            sb.append(" [Regexp]");
        }
        if (this.searchBackwards) {
            sb.append(" [Backwards]");
        }
        if (this.searchTerm != null) {
            sb.append(" [");
            sb.append(this.searchTerm);
            sb.append("]");
        }
        sb.append(": ");
        return sb.toString();
    }
    
    String computeCurPos() {
        int chari = 0;
        int chart = 0;
        for (int i = 0; i < this.buffer.lines.size(); ++i) {
            final int l = this.buffer.lines.get(i).length() + 1;
            if (i < this.buffer.line) {
                chari += l;
            }
            else if (i == this.buffer.line) {
                chari += this.buffer.offsetInLine + this.buffer.column;
            }
            chart += l;
        }
        final StringBuilder sb = new StringBuilder();
        sb.append("line ");
        sb.append(this.buffer.line + 1);
        sb.append("/");
        sb.append(this.buffer.lines.size());
        sb.append(" (");
        sb.append(Math.round(100.0 * this.buffer.line / this.buffer.lines.size()));
        sb.append("%), ");
        sb.append("col ");
        sb.append(this.buffer.offsetInLine + this.buffer.column + 1);
        sb.append("/");
        sb.append(this.buffer.length(this.buffer.lines.get(this.buffer.line)) + 1);
        sb.append(" (");
        if (!this.buffer.lines.get(this.buffer.line).isEmpty()) {
            sb.append(Math.round(100.0 * (this.buffer.offsetInLine + this.buffer.column) / this.buffer.length(this.buffer.lines.get(this.buffer.line))));
        }
        else {
            sb.append("100");
        }
        sb.append("%), ");
        sb.append("char ");
        sb.append(chari + 1);
        sb.append("/");
        sb.append(chart);
        sb.append(" (");
        sb.append(Math.round(100.0 * chari / chart));
        sb.append("%)");
        return sb.toString();
    }
    
    void curPos() {
        this.setMessage(this.computeCurPos());
    }
    
    void prevBuffer() throws IOException {
        if (this.buffers.size() > 1) {
            this.bufferIndex = (this.bufferIndex + this.buffers.size() - 1) % this.buffers.size();
            this.buffer = this.buffers.get(this.bufferIndex);
            this.setMessage("Switched to " + this.buffer.getTitle());
            this.buffer.open();
            this.display.clear();
        }
        else {
            this.setMessage("No more open file buffers");
        }
    }
    
    void nextBuffer() throws IOException {
        if (this.buffers.size() > 1) {
            this.bufferIndex = (this.bufferIndex + 1) % this.buffers.size();
            this.buffer = this.buffers.get(this.bufferIndex);
            this.setMessage("Switched to " + this.buffer.getTitle());
            this.buffer.open();
            this.display.clear();
        }
        else {
            this.setMessage("No more open file buffers");
        }
    }
    
    void setMessage(final String message) {
        this.message = message;
        this.nbBindings = (this.quickBlank ? 2 : 25);
    }
    
    boolean quit() throws IOException {
        if (this.buffer.dirty) {
            if (this.tempFile) {
                if (!this.write()) {
                    return false;
                }
            }
            else {
                final Operation op = this.getYNC("Save modified buffer (ANSWERING \"No\" WILL DESTROY CHANGES) ? ");
                switch (op.ordinal()) {
                    case 37: {
                        return false;
                    }
                    case 46: {
                        if (!this.write()) {
                            return false;
                        }
                        break;
                    }
                }
            }
        }
        this.buffers.remove(this.bufferIndex);
        if (this.bufferIndex == this.buffers.size() && this.bufferIndex > 0) {
            this.bufferIndex = this.buffers.size() - 1;
        }
        if (this.buffers.isEmpty()) {
            this.buffer = null;
            return true;
        }
        (this.buffer = this.buffers.get(this.bufferIndex)).open();
        this.display.clear();
        this.setMessage("Switched to " + this.buffer.getTitle());
        return false;
    }
    
    void numbers() {
        this.printLineNumbers = !this.printLineNumbers;
        this.resetDisplay();
        this.setMessage("Lines numbering " + (this.printLineNumbers ? "enabled" : "disabled"));
    }
    
    void smoothScrolling() {
        this.smoothScrolling = !this.smoothScrolling;
        this.setMessage("Smooth scrolling " + (this.smoothScrolling ? "enabled" : "disabled"));
    }
    
    void mouseSupport() {
        this.mouseSupport = !this.mouseSupport;
        this.setMessage("Mouse support " + (this.mouseSupport ? "enabled" : "disabled"));
        this.terminal.trackMouse(this.mouseSupport ? Terminal.MouseTracking.Normal : Terminal.MouseTracking.Off);
    }
    
    void constantCursor() {
        this.constantCursor = !this.constantCursor;
        this.setMessage("Constant cursor position display " + (this.constantCursor ? "enabled" : "disabled"));
    }
    
    void oneMoreLine() {
        this.oneMoreLine = !this.oneMoreLine;
        this.setMessage("Use of one more line for editing " + (this.oneMoreLine ? "enabled" : "disabled"));
    }
    
    void wrap() {
        this.wrapping = !this.wrapping;
        this.buffer.computeAllOffsets();
        this.resetDisplay();
        this.setMessage("Lines wrapping " + (this.wrapping ? "enabled" : "disabled"));
    }
    
    void clearScreen() {
        this.resetDisplay();
    }
    
    void mouseEvent() {
        final Terminal terminal = this.terminal;
        final BindingReader bindingReader = this.bindingReader;
        Objects.requireNonNull(bindingReader);
        final MouseEvent event = terminal.readMouseEvent(bindingReader::readCharacter, this.bindingReader.getLastBinding());
        if (!this.mouseSupport) {
            return;
        }
        final MouseEvent.Type eventType = event.getType();
        if (eventType == MouseEvent.Type.Released && event.getModifiers().isEmpty() && event.getButton() == MouseEvent.Button.Button1) {
            final int x = event.getX();
            final int y = event.getY();
            final int hdr = this.buffer.computeHeader().size();
            final int ftr = this.computeFooter().size();
            if (y >= hdr) {
                if (y < this.size.getRows() - ftr) {
                    this.buffer.moveTo(x, y - hdr);
                }
                else {
                    final int cols = (this.shortcuts.size() + 1) / 2;
                    final int cw = this.size.getColumns() / cols;
                    final int l = y - (this.size.getRows() - ftr) - 1;
                    int si = l * cols + x / cw;
                    String shortcut = null;
                    final Iterator<String> it = this.shortcuts.keySet().iterator();
                    while (si-- >= 0 && it.hasNext()) {
                        shortcut = it.next();
                    }
                    if (shortcut != null) {
                        shortcut = shortcut.replaceAll("M-", "\\\\E");
                        final String seq = KeyMap.translate(shortcut);
                        this.bindingReader.runMacro(seq);
                    }
                }
            }
        }
        else if (eventType == MouseEvent.Type.Wheel) {
            if (event.getButton() == MouseEvent.Button.WheelDown) {
                this.buffer.moveDown(1);
            }
            else if (event.getButton() == MouseEvent.Button.WheelUp) {
                this.buffer.moveUp(1);
            }
        }
        else if (eventType == MouseEvent.Type.Moved) {
            this.mouseX = event.getX();
            this.mouseY = event.getY();
        }
    }
    
    void enableSuspension() {
        if (!this.restricted && this.vsusp < 0) {
            final Attributes attrs = this.terminal.getAttributes();
            attrs.setControlChar(Attributes.ControlChar.VSUSP, this.vsusp);
            this.terminal.setAttributes(attrs);
        }
    }
    
    void toggleSuspension() {
        if (this.restricted) {
            this.setMessage("This function is disabled in restricted mode");
        }
        else if (this.vsusp < 0) {
            this.setMessage("This function is disabled");
        }
        else {
            final Attributes attrs = this.terminal.getAttributes();
            int toggle = this.vsusp;
            String message = "enabled";
            if (attrs.getControlChar(Attributes.ControlChar.VSUSP) > 0) {
                toggle = 0;
                message = "disabled";
            }
            attrs.setControlChar(Attributes.ControlChar.VSUSP, toggle);
            this.terminal.setAttributes(attrs);
            this.setMessage("Suspension " + message);
        }
    }
    
    public String getTitle() {
        return this.title;
    }
    
    void resetDisplay() {
        this.display.clear();
        this.display.resize(this.size.getRows(), this.size.getColumns());
        for (final Buffer buffer : this.buffers) {
            buffer.resetDisplay();
        }
    }
    
    synchronized void display() {
        this.display(null);
    }
    
    synchronized void display(final Integer editCursor) {
        if (this.nbBindings > 0 && --this.nbBindings == 0) {
            this.message = null;
        }
        final List<AttributedString> header = this.buffer.computeHeader();
        final List<AttributedString> footer = this.computeFooter();
        final int nbLines = this.size.getRows() - header.size() - footer.size();
        if (this.insertHelp) {
            this.insertHelp(this.suggestionBox.getSelected());
            this.resetSuggestion();
        }
        final List<Diagnostic> diagnostics = this.computeDiagnostic();
        final List<AttributedString> newLines = this.buffer.getDisplayedLines(nbLines, diagnostics);
        if (this.help) {
            this.showCompletion(newLines);
        }
        newLines.addAll(0, header);
        newLines.addAll(footer);
        int cursor;
        if (this.editMessage != null) {
            final int crsr = (editCursor != null) ? editCursor : this.editBuffer.length();
            cursor = this.editMessage.length() + crsr;
            cursor = this.size.cursorPos(this.size.getRows() - footer.size(), cursor);
        }
        else {
            cursor = this.size.cursorPos(header.size(), this.buffer.getDisplayedCursor());
        }
        this.display.update(newLines, cursor);
        if (this.windowsTerminal) {
            this.resetDisplay();
        }
    }
    
    protected void insertHelp(final int selected) {
    }
    
    private void showCompletion(final List<AttributedString> newLines) {
        if (this.suggestions == null) {
            final LinkedHashMap<AttributedString, List<AttributedString>> result = this.computeSuggestions();
            if (result == null || result.isEmpty()) {
                this.resetSuggestion();
                return;
            }
            this.suggestions = result;
        }
        this.initBoxes(newLines);
    }
    
    protected LinkedHashMap<AttributedString, List<AttributedString>> computeSuggestions() {
        return new LinkedHashMap<AttributedString, List<AttributedString>>();
    }
    
    protected List<Diagnostic> computeDiagnostic() {
        return Collections.emptyList();
    }
    
    private void initBoxes(final List<AttributedString> screenLines) {
        final List<AttributedString> suggestionList = new ArrayList<AttributedString>(this.suggestions.keySet());
        if (this.suggestionBox == null) {
            this.suggestionBox = this.buildSuggestionBox(suggestionList, screenLines);
        }
        if (this.suggestionBox != null) {
            this.suggestionBox.draw(screenLines);
            final int selectedIndex = this.suggestionBox.getSelected();
            if (selectedIndex >= 0 && selectedIndex < suggestionList.size()) {
                final AttributedString selectedSuggestion = suggestionList.get(selectedIndex);
                final List<AttributedString> documentation = this.suggestions.get(selectedSuggestion);
                if (documentation != null && !documentation.isEmpty()) {
                    final Box documentationBox = this.buildDocumentationBox(screenLines, this.suggestionBox, documentation);
                    if (documentationBox != null) {
                        documentationBox.draw(screenLines);
                    }
                }
            }
        }
    }
    
    private Box buildSuggestionBox(final List<AttributedString> suggestions, final List<AttributedString> screenLines) {
        if (suggestions == null || suggestions.isEmpty()) {
            return null;
        }
        final int cursorX = this.buffer.column;
        final int xi = Math.max(this.printLineNumbers ? 8 : 0, cursorX);
        final int maxSuggestionLength = suggestions.stream().mapToInt(AttributedString::length).max().orElse(10) + 2;
        final int maxScreenWidth = (int)Math.round(this.size.getColumns() * 0.6);
        int xl = Math.min(xi + maxSuggestionLength, xi + maxScreenWidth);
        xl = Math.min(xl, this.size.getColumns() - 1);
        final int maxHeight = screenLines.size() - 1;
        int maxVisibleItems = Math.min(10, maxHeight / 3);
        maxVisibleItems = Math.max(2, maxVisibleItems);
        int requiredHeight = Math.min(suggestions.size(), maxVisibleItems) + 2;
        final int cursorLine = this.buffer.line;
        final int cursorScreenLine = cursorLine - this.buffer.firstLineToDisplay;
        int yi = cursorScreenLine + 1;
        final int spaceBelow = maxHeight - yi;
        boolean displayBelow = true;
        if (spaceBelow < requiredHeight) {
            final int spaceAbove = cursorScreenLine;
            if (spaceAbove > spaceBelow || spaceBelow < 3) {
                displayBelow = false;
                yi = Math.max(0, cursorScreenLine - requiredHeight - 1);
                if (cursorScreenLine - requiredHeight - 1 < 0) {
                    requiredHeight = Math.max(3, cursorScreenLine - 1);
                    yi = 0;
                }
            }
            else {
                requiredHeight = Math.max(3, spaceBelow);
            }
        }
        int yl;
        if (displayBelow) {
            yl = Math.min(maxHeight, yi + requiredHeight);
        }
        else {
            yl = cursorScreenLine - 1;
        }
        if (yl <= yi || xl <= xi) {
            return null;
        }
        if (xl - xi < 4) {
            xl = xi + 4;
        }
        final Box box = new Box(xi, yi, xl, yl);
        box.setLines(suggestions);
        box.setSelectedStyle(AttributedStyle.DEFAULT.background(4).foreground(7));
        return box;
    }
    
    private Box buildDocumentationBox(final List<AttributedString> screenLines, final Box suggestionBox, List<AttributedString> documentation) {
        if (suggestionBox == null || screenLines == null || screenLines.isEmpty()) {
            return null;
        }
        if (documentation == null || documentation.isEmpty()) {
            return null;
        }
        final int dXi = suggestionBox.xl;
        final int dBoxSize = documentation.stream().mapToInt(AttributedString::length).max().orElse(10) + 2;
        final int xi = Math.max(this.printLineNumbers ? 9 : 1, dXi);
        final int maxWidth = (int)Math.round((this.size.getColumns() - xi) * 0.6);
        final int xl = Math.min(dBoxSize + xi, xi + maxWidth);
        if (xl <= xi) {
            return null;
        }
        documentation = this.adjustLines(documentation, dBoxSize - 2, xl - xi - 2);
        final int height = screenLines.size();
        final int requiredHeight = documentation.size() + 2;
        final int yi = suggestionBox.yi;
        int yl = yi + requiredHeight;
        if (yl >= height) {
            yl = Math.min(height - 1, yl);
            if (yl - yi < 3) {
                return null;
            }
        }
        if (yl <= yi) {
            return null;
        }
        final Box documentationBox = new Box(xi, yi, xl, yl);
        documentationBox.setLines(documentation);
        documentationBox.setSelectedStyle(AttributedStyle.DEFAULT);
        return documentationBox;
    }
    
    private List<AttributedString> adjustLines(final List<AttributedString> lines, final int max, final int boxLength) {
        if (max <= boxLength) {
            return lines;
        }
        final List<AttributedString> adjustedLines = new ArrayList<AttributedString>();
        for (final AttributedString line : lines) {
            if (line.length() < boxLength) {
                adjustedLines.add(line);
            }
            else {
                int end;
                for (int start = 0; start < line.length(); start = end) {
                    final int stepSize = end = Math.min(start + boxLength, line.length());
                    if (end - start >= boxLength) {
                        while (end > start && !Character.isWhitespace(line.charAt(end - 1))) {
                            --end;
                        }
                    }
                    if (end == start) {
                        end = stepSize;
                    }
                    adjustedLines.add(line.substring(start, end));
                }
            }
        }
        return adjustedLines;
    }
    
    protected List<AttributedString> computeFooter() {
        final List<AttributedString> footer = new ArrayList<AttributedString>();
        if (this.editMessage != null) {
            final AttributedStringBuilder sb = new AttributedStringBuilder();
            sb.style(AttributedStyle.INVERSE);
            sb.append(this.editMessage);
            sb.append(this.editBuffer);
            for (int i = this.editMessage.length() + this.editBuffer.length(); i < this.size.getColumns(); ++i) {
                sb.append(' ');
            }
            sb.append('\n');
            footer.add(sb.toAttributedString());
        }
        else if (this.message != null || this.constantCursor) {
            final int rwidth = this.size.getColumns();
            final String text = "[ " + ((this.message == null) ? this.computeCurPos() : this.message) + " ]";
            final int len = text.length();
            final AttributedStringBuilder sb2 = new AttributedStringBuilder();
            for (int j = 0; j < (rwidth - len) / 2; ++j) {
                sb2.append(' ');
            }
            sb2.style(AttributedStyle.INVERSE);
            sb2.append(text);
            sb2.append('\n');
            footer.add(sb2.toAttributedString());
        }
        else {
            footer.add(new AttributedString("\n"));
        }
        final Iterator<Map.Entry<String, String>> sit = this.shortcuts.entrySet().iterator();
        final int cols = (this.shortcuts.size() + 1) / 2;
        final int cw = (this.size.getColumns() - 1) / cols;
        final int rem = (this.size.getColumns() - 1) % cols;
        for (int l = 0; l < 2; ++l) {
            final AttributedStringBuilder sb3 = new AttributedStringBuilder();
            for (int c = 0; c < cols; ++c) {
                final Map.Entry<String, String> entry = sit.hasNext() ? sit.next() : null;
                final String key = (entry != null) ? entry.getKey() : "";
                final String val = (entry != null) ? entry.getValue() : "";
                sb3.style(AttributedStyle.INVERSE);
                sb3.append(key);
                sb3.style(AttributedStyle.DEFAULT);
                sb3.append(" ");
                final int nb = cw - key.length() - 1 + ((c < rem) ? 1 : 0);
                if (val.length() > nb) {
                    sb3.append(val.substring(0, nb));
                }
                else {
                    sb3.append(val);
                    if (c < cols - 1) {
                        for (int k = 0; k < nb - val.length(); ++k) {
                            sb3.append(" ");
                        }
                    }
                }
            }
            sb3.append('\n');
            footer.add(sb3.toAttributedString());
        }
        return footer;
    }
    
    protected void handle(final Terminal.Signal signal) {
        if (this.buffer != null) {
            this.size.copy(this.terminal.getSize());
            this.buffer.computeAllOffsets();
            this.buffer.moveToChar(this.buffer.offsetInLine + this.buffer.column);
            this.resetDisplay();
            this.display();
        }
    }
    
    protected void bindKeys() {
        this.keys = new KeyMap<Operation>();
        if (!this.view) {
            this.keys.setUnicode(Operation.INSERT);
            for (char i = ' '; i < '\u0080'; ++i) {
                this.keys.bind(Operation.INSERT, Character.toString(i));
            }
            this.keys.bind(Operation.BACKSPACE, KeyMap.del());
            for (char i = 'A'; i <= 'Z'; ++i) {
                this.keys.bind(Operation.DO_LOWER_CASE, KeyMap.alt(i));
            }
            this.keys.bind(Operation.WRITE, KeyMap.ctrl('O'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f3));
            this.keys.bind(Operation.JUSTIFY_PARAGRAPH, KeyMap.ctrl('J'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f4));
            this.keys.bind(Operation.CUT, KeyMap.ctrl('K'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f9));
            this.keys.bind(Operation.UNCUT, KeyMap.ctrl('U'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f10));
            this.keys.bind(Operation.REPLACE, KeyMap.ctrl('\\'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f14), KeyMap.alt('r'));
            this.keys.bind(Operation.MARK, KeyMap.ctrl('^'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f15), KeyMap.alt('a'));
            this.keys.bind(Operation.COPY, KeyMap.alt('^'), KeyMap.alt('6'));
            this.keys.bind(Operation.INDENT, KeyMap.alt('}'));
            this.keys.bind(Operation.UNINDENT, KeyMap.alt('{'));
            this.keys.bind(Operation.VERBATIM, KeyMap.alt('v'));
            this.keys.bind(Operation.INSERT, KeyMap.ctrl('I'), KeyMap.ctrl('M'));
            this.keys.bind(Operation.DELETE, KeyMap.ctrl('D'), KeyMap.key(this.terminal, InfoCmp.Capability.key_dc));
            this.keys.bind(Operation.BACKSPACE, KeyMap.ctrl('H'));
            this.keys.bind(Operation.CUT_TO_END, KeyMap.alt('t'));
            this.keys.bind(Operation.JUSTIFY_FILE, KeyMap.alt('j'));
            this.keys.bind(Operation.AUTO_INDENT, KeyMap.alt('i'));
            this.keys.bind(Operation.CUT_TO_END_TOGGLE, KeyMap.alt('k'));
            this.keys.bind(Operation.TABS_TO_SPACE, KeyMap.alt('q'));
        }
        else {
            this.keys.bind(Operation.NEXT_PAGE, " ", "f");
            this.keys.bind(Operation.PREV_PAGE, "b");
        }
        this.keys.bind(Operation.NEXT_PAGE, KeyMap.ctrl('V'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f8));
        this.keys.bind(Operation.PREV_PAGE, KeyMap.ctrl('Y'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f7));
        this.keys.bind(Operation.HELP, KeyMap.ctrl('G'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f1));
        this.keys.bind(Operation.QUIT, KeyMap.ctrl('X'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f2));
        this.keys.bind(Operation.READ, KeyMap.ctrl('R'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f5));
        this.keys.bind(Operation.SEARCH, KeyMap.ctrl('W'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f6));
        this.keys.bind(Operation.CUR_POS, KeyMap.ctrl('C'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f11));
        this.keys.bind(Operation.TO_SPELL, KeyMap.ctrl('T'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f11));
        this.keys.bind(Operation.GOTO, KeyMap.ctrl('_'), KeyMap.key(this.terminal, InfoCmp.Capability.key_f13), KeyMap.alt('g'));
        this.keys.bind(Operation.NEXT_SEARCH, KeyMap.key(this.terminal, InfoCmp.Capability.key_f16), KeyMap.alt('w'));
        this.keys.bind(Operation.RIGHT, KeyMap.ctrl('F'));
        this.keys.bind(Operation.LEFT, KeyMap.ctrl('B'));
        this.keys.bind(Operation.NEXT_WORD, KeyMap.translate("^[[1;5C"));
        this.keys.bind(Operation.PREV_WORD, KeyMap.translate("^[[1;5D"));
        this.keys.bind(Operation.NEXT_WORD, KeyMap.alt(KeyMap.key(this.terminal, InfoCmp.Capability.key_right)));
        this.keys.bind(Operation.PREV_WORD, KeyMap.alt(KeyMap.key(this.terminal, InfoCmp.Capability.key_left)));
        this.keys.bind(Operation.NEXT_WORD, KeyMap.alt(KeyMap.translate("^[[C")));
        this.keys.bind(Operation.PREV_WORD, KeyMap.alt(KeyMap.translate("^[[D")));
        this.keys.bind(Operation.LSP_SUGGESTION, KeyMap.ctrl(' '));
        this.keys.bind(Operation.UP, KeyMap.ctrl('P'));
        this.keys.bind(Operation.DOWN, KeyMap.ctrl('N'));
        this.keys.bind(Operation.BEGINNING_OF_LINE, KeyMap.ctrl('A'), KeyMap.key(this.terminal, InfoCmp.Capability.key_home));
        this.keys.bind(Operation.END_OF_LINE, KeyMap.ctrl('E'), KeyMap.key(this.terminal, InfoCmp.Capability.key_end));
        this.keys.bind(Operation.BEGINNING_OF_PARAGRAPH, KeyMap.alt('('), KeyMap.alt('9'));
        this.keys.bind(Operation.END_OF_PARAGRAPH, KeyMap.alt(')'), KeyMap.alt('0'));
        this.keys.bind(Operation.FIRST_LINE, KeyMap.alt('\\'), KeyMap.alt('|'));
        this.keys.bind(Operation.LAST_LINE, KeyMap.alt('/'), KeyMap.alt('?'));
        this.keys.bind(Operation.MATCHING, KeyMap.alt(']'));
        this.keys.bind(Operation.SCROLL_UP, KeyMap.alt('-'), KeyMap.alt('_'));
        this.keys.bind(Operation.SCROLL_DOWN, KeyMap.alt('+'), KeyMap.alt('='));
        this.keys.bind(Operation.PREV_BUFFER, KeyMap.alt('<'));
        this.keys.bind(Operation.NEXT_BUFFER, KeyMap.alt('>'));
        this.keys.bind(Operation.PREV_BUFFER, KeyMap.alt(','));
        this.keys.bind(Operation.NEXT_BUFFER, KeyMap.alt('.'));
        this.keys.bind(Operation.COUNT, KeyMap.alt('d'));
        this.keys.bind(Operation.CLEAR_SCREEN, KeyMap.ctrl('L'));
        this.keys.bind(Operation.HELP, KeyMap.alt('x'));
        this.keys.bind(Operation.CONSTANT_CURSOR, KeyMap.alt('c'));
        this.keys.bind(Operation.ONE_MORE_LINE, KeyMap.alt('o'));
        this.keys.bind(Operation.SMOOTH_SCROLLING, KeyMap.alt('s'));
        this.keys.bind(Operation.MOUSE_SUPPORT, KeyMap.alt('m'));
        this.keys.bind(Operation.WHITESPACE, KeyMap.alt('p'));
        this.keys.bind(Operation.HIGHLIGHT, KeyMap.alt('y'));
        this.keys.bind(Operation.SMART_HOME_KEY, KeyMap.alt('h'));
        this.keys.bind(Operation.WRAP, KeyMap.alt('l'));
        this.keys.bind(Operation.BACKUP, KeyMap.alt('b'));
        this.keys.bind(Operation.NUMBERS, KeyMap.alt('n'));
        this.keys.bind(Operation.UP, KeyMap.key(this.terminal, InfoCmp.Capability.key_up));
        this.keys.bind(Operation.DOWN, KeyMap.key(this.terminal, InfoCmp.Capability.key_down));
        this.keys.bind(Operation.RIGHT, KeyMap.key(this.terminal, InfoCmp.Capability.key_right));
        this.keys.bind(Operation.LEFT, KeyMap.key(this.terminal, InfoCmp.Capability.key_left));
        this.keys.bind(Operation.MOUSE_EVENT, (CharSequence[])MouseSupport.keys(this.terminal));
        this.keys.bind(Operation.TOGGLE_SUSPENSION, KeyMap.alt('z'));
        this.keys.bind(Operation.NEXT_PAGE, KeyMap.key(this.terminal, InfoCmp.Capability.key_npage));
        this.keys.bind(Operation.PREV_PAGE, KeyMap.key(this.terminal, InfoCmp.Capability.key_ppage));
    }
    
    protected enum WriteMode
    {
        WRITE, 
        APPEND, 
        PREPEND;
    }
    
    protected enum WriteFormat
    {
        UNIX, 
        DOS, 
        MAC;
    }
    
    protected enum CursorMovement
    {
        RIGHT, 
        LEFT, 
        STILL;
    }
    
    protected class Buffer
    {
        String file;
        Charset charset;
        WriteFormat format;
        List<String> lines;
        int firstLineToDisplay;
        int firstColumnToDisplay;
        int offsetInLineToDisplay;
        int line;
        List<LinkedList<Integer>> offsets;
        int offsetInLine;
        int column;
        int wantedColumn;
        boolean uncut;
        int[] markPos;
        SyntaxHighlighter syntaxHighlighter;
        boolean dirty;
        
        protected Buffer(final String file) {
            this.format = WriteFormat.UNIX;
            this.firstColumnToDisplay = 0;
            this.offsets = new ArrayList<LinkedList<Integer>>();
            this.uncut = false;
            this.markPos = new int[] { -1, -1 };
            this.file = file;
            this.syntaxHighlighter = SyntaxHighlighter.build(Nano.this.syntaxFiles, file, Nano.this.syntaxName, Nano.this.nanorcIgnoreErrors);
        }
        
        public void setDirty(final boolean dirty) {
            this.dirty = dirty;
        }
        
        public String getFile() {
            return this.file;
        }
        
        public List<String> getLines() {
            return this.lines;
        }
        
        public int getFirstLineToDisplay() {
            return this.firstLineToDisplay;
        }
        
        public int getFirstColumnToDisplay() {
            return this.firstColumnToDisplay;
        }
        
        public int getOffsetInLineToDisplay() {
            return this.offsetInLineToDisplay;
        }
        
        public int getLine() {
            return this.line;
        }
        
        public Charset getCharset() {
            return this.charset;
        }
        
        public WriteFormat getFormat() {
            return this.format;
        }
        
        public boolean isDirty() {
            return this.dirty;
        }
        
        public SyntaxHighlighter getSyntaxHighlighter() {
            return this.syntaxHighlighter;
        }
        
        public int getOffsetInLine() {
            return this.offsetInLine;
        }
        
        public int getColumn() {
            return this.column;
        }
        
        public void open() throws IOException {
            if (this.lines != null) {
                return;
            }
            (this.lines = new ArrayList<String>()).add("");
            this.charset = Charset.defaultCharset();
            this.computeAllOffsets();
            if (this.file == null) {
                return;
            }
            final Path path = Nano.this.root.resolve(this.file);
            if (Files.isDirectory(path, new LinkOption[0])) {
                Nano.this.setMessage("\"" + this.file + "\" is a directory");
                return;
            }
            try (final InputStream fis = Files.newInputStream(path, new OpenOption[0])) {
                this.read(fis);
            }
            catch (final IOException e) {
                Nano.this.setMessage("Error reading " + this.file + ": " + e.getMessage());
            }
        }
        
        public void open(final InputStream is) throws IOException {
            if (this.lines != null) {
                return;
            }
            (this.lines = new ArrayList<String>()).add("");
            this.charset = Charset.defaultCharset();
            this.computeAllOffsets();
            this.read(is);
        }
        
        public void read(final InputStream fis) throws IOException {
            final ByteArrayOutputStream bos = new ByteArrayOutputStream();
            final byte[] buffer = new byte[4096];
            int remaining;
            while ((remaining = fis.read(buffer)) > 0) {
                bos.write(buffer, 0, remaining);
            }
            final byte[] bytes = bos.toByteArray();
            try {
                final UniversalDetector detector = new UniversalDetector((CharsetListener)null);
                detector.handleData(bytes, 0, bytes.length);
                detector.dataEnd();
                if (detector.getDetectedCharset() != null) {
                    this.charset = Charset.forName(detector.getDetectedCharset());
                }
            }
            catch (final Throwable t) {}
            try (final BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(bytes), this.charset))) {
                this.lines.clear();
                String line;
                while ((line = reader.readLine()) != null) {
                    this.lines.add(line);
                }
            }
            if (this.lines.isEmpty()) {
                this.lines.add("");
            }
            this.computeAllOffsets();
            this.moveToChar(0);
        }
        
        private int charPosition(final int displayPosition) {
            return this.charPosition(this.line, displayPosition, CursorMovement.STILL);
        }
        
        private int charPosition(final int displayPosition, final CursorMovement move) {
            return this.charPosition(this.line, displayPosition, move);
        }
        
        private int charPosition(final int line, final int displayPosition) {
            return this.charPosition(line, displayPosition, CursorMovement.STILL);
        }
        
        private int charPosition(final int line, final int displayPosition, final CursorMovement move) {
            int out = this.lines.get(line).length();
            if (!this.lines.get(line).contains("\t") || displayPosition == 0) {
                out = displayPosition;
            }
            else if (displayPosition < this.length(this.lines.get(line))) {
                int rdiff = 0;
                int ldiff = 0;
                for (int i = 0; i < this.lines.get(line).length(); ++i) {
                    final int dp = this.length(this.lines.get(line).substring(0, i));
                    if (move == CursorMovement.LEFT) {
                        if (dp > displayPosition) {
                            break;
                        }
                        out = i;
                    }
                    else if (move == CursorMovement.RIGHT) {
                        if (dp >= displayPosition) {
                            out = i;
                            break;
                        }
                    }
                    else if (move == CursorMovement.STILL) {
                        if (dp <= displayPosition) {
                            ldiff = displayPosition - dp;
                            out = i;
                        }
                        else {
                            rdiff = dp - displayPosition;
                            if (rdiff < ldiff) {
                                out = i;
                                break;
                            }
                            break;
                        }
                    }
                }
            }
            return out;
        }
        
        String blanks(final int nb) {
            final StringBuilder sb = new StringBuilder();
            for (int i = 0; i < nb; ++i) {
                sb.append(' ');
            }
            return sb.toString();
        }
        
        public void insert(String insert) {
            final String text = this.lines.get(this.line);
            final int pos = this.charPosition(this.offsetInLine + this.column);
            insert = insert.replaceAll("\r\n", "\n");
            insert = insert.replaceAll("\r", "\n");
            if (Nano.this.tabsToSpaces && insert.length() == 1 && insert.charAt(0) == '\t') {
                final int len = (pos == text.length()) ? this.length(text + insert) : this.length(text.substring(0, pos) + insert);
                insert = this.blanks(len - this.offsetInLine - this.column);
            }
            if (Nano.this.autoIndent && insert.length() == 1 && insert.charAt(0) == '\n') {
                String currentLine;
                int indentLength;
                for (currentLine = this.lines.get(this.line), indentLength = 0; indentLength < currentLine.length() && (currentLine.charAt(indentLength) == ' ' || currentLine.charAt(indentLength) == '\t'); ++indentLength) {}
                if (indentLength > 0) {
                    insert += currentLine.substring(0, indentLength);
                }
            }
            String tail = "";
            String mod;
            if (pos == text.length()) {
                mod = text + insert;
            }
            else {
                mod = text.substring(0, pos) + insert;
                tail = text.substring(pos);
            }
            final List<String> ins = new ArrayList<String>();
            int last = 0;
            for (int idx = mod.indexOf(10, last); idx >= 0; idx = mod.indexOf(10, last)) {
                ins.add(mod.substring(last, idx));
                last = idx + 1;
            }
            ins.add(mod.substring(last) + tail);
            final int curPos = this.length(mod.substring(last));
            this.lines.set(this.line, ins.get(0));
            this.offsets.set(this.line, this.computeOffsets(ins.get(0)));
            for (int i = 1; i < ins.size(); ++i) {
                ++this.line;
                this.lines.add(this.line, ins.get(i));
                this.offsets.add(this.line, this.computeOffsets(ins.get(i)));
            }
            this.moveToChar(curPos);
            this.ensureCursorVisible();
            this.dirty = true;
        }
        
        void computeAllOffsets() {
            this.offsets.clear();
            for (final String text : this.lines) {
                this.offsets.add(this.computeOffsets(text));
            }
        }
        
        LinkedList<Integer> computeOffsets(final String line) {
            final String text = new AttributedStringBuilder().tabs(Nano.this.tabs).append(line).toString();
            final int width = Nano.this.size.getColumns() - (Nano.this.printLineNumbers ? 8 : 0);
            final LinkedList<Integer> offsets = new LinkedList<Integer>();
            offsets.add(0);
            if (Nano.this.wrapping) {
                int last = 0;
                int prevword = 0;
                boolean inspace = false;
                for (int i = 0; i < text.length(); ++i) {
                    if (this.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;
                    }
                }
            }
            return offsets;
        }
        
        public boolean isBreakable(final char ch) {
            return !Nano.this.atBlanks || ch == ' ';
        }
        
        public void moveToChar(final int pos) {
            this.moveToChar(pos, CursorMovement.STILL);
        }
        
        public void moveToChar(int pos, final CursorMovement move) {
            if (!Nano.this.wrapping) {
                if (pos > this.column && pos - this.firstColumnToDisplay + 1 > this.width()) {
                    this.firstColumnToDisplay = this.offsetInLine + this.column - 6;
                }
                else if (pos < this.column && this.firstColumnToDisplay + 5 > pos) {
                    this.firstColumnToDisplay = Math.max(0, this.firstColumnToDisplay - this.width() + 5);
                }
            }
            if (this.lines.get(this.line).contains("\t")) {
                final int cpos = this.charPosition(pos, move);
                if (cpos < this.lines.get(this.line).length()) {
                    pos = this.length(this.lines.get(this.line).substring(0, cpos));
                }
                else {
                    pos = this.length(this.lines.get(this.line));
                }
            }
            this.offsetInLine = this.prevLineOffset(this.line, pos + 1).get();
            this.column = pos - this.offsetInLine;
        }
        
        public void delete(int count) {
            while (--count >= 0 && this.moveRight(1) && this.backspace(1)) {}
        }
        
        public boolean backspace(int count) {
            while (count > 0) {
                String text = this.lines.get(this.line);
                final int pos = this.charPosition(this.offsetInLine + this.column);
                if (pos == 0) {
                    if (this.line == 0) {
                        this.bof();
                        return false;
                    }
                    final List<String> lines = this.lines;
                    final int line = this.line - 1;
                    this.line = line;
                    final String prev = lines.get(line);
                    this.lines.set(this.line, prev + text);
                    this.offsets.set(this.line, this.computeOffsets(prev + text));
                    this.moveToChar(this.length(prev));
                    this.lines.remove(this.line + 1);
                    this.offsets.remove(this.line + 1);
                    --count;
                }
                else {
                    final int nb = Math.min(pos, count);
                    final int curPos = this.length(text.substring(0, pos - nb));
                    text = text.substring(0, pos - nb) + text.substring(pos);
                    this.lines.set(this.line, text);
                    this.offsets.set(this.line, this.computeOffsets(text));
                    this.moveToChar(curPos);
                    count -= nb;
                }
                this.dirty = true;
            }
            this.ensureCursorVisible();
            return true;
        }
        
        public boolean moveLeft(int chars) {
            boolean ret = true;
            while (--chars >= 0) {
                if (this.offsetInLine + this.column > 0) {
                    this.moveToChar(this.offsetInLine + this.column - 1, CursorMovement.LEFT);
                }
                else {
                    if (this.line <= 0) {
                        this.bof();
                        ret = false;
                        break;
                    }
                    --this.line;
                    this.moveToChar(this.length(this.getLine(this.line)));
                }
            }
            this.wantedColumn = this.column;
            this.ensureCursorVisible();
            return ret;
        }
        
        public boolean moveRight(final int chars) {
            return this.moveRight(chars, false);
        }
        
        public int width() {
            return Nano.this.size.getColumns() - (Nano.this.printLineNumbers ? 8 : 0) - (Nano.this.wrapping ? 0 : 1) - ((this.firstColumnToDisplay > 0) ? 1 : 0);
        }
        
        public boolean moveRight(int chars, final boolean fromBeginning) {
            if (fromBeginning) {
                this.firstColumnToDisplay = 0;
                this.offsetInLine = 0;
                this.column = 0;
                chars = Math.min(chars, this.length(this.getLine(this.line)));
            }
            boolean ret = true;
            while (--chars >= 0) {
                final int len = this.length(this.getLine(this.line));
                if (this.offsetInLine + this.column + 1 <= len) {
                    this.moveToChar(this.offsetInLine + this.column + 1, CursorMovement.RIGHT);
                }
                else {
                    if (this.getLine(this.line + 1) == null) {
                        this.eof();
                        ret = false;
                        break;
                    }
                    ++this.line;
                    this.firstColumnToDisplay = 0;
                    this.offsetInLine = 0;
                    this.column = 0;
                }
            }
            this.wantedColumn = this.column;
            this.ensureCursorVisible();
            return ret;
        }
        
        public void moveDown(final int lines) {
            this.cursorDown(lines);
            this.ensureCursorVisible();
        }
        
        public void moveUp(final int lines) {
            this.cursorUp(lines);
            this.ensureCursorVisible();
        }
        
        private Optional<Integer> prevLineOffset(final int line, final int offsetInLine) {
            if (line >= this.offsets.size()) {
                return Optional.empty();
            }
            final Iterator<Integer> it = this.offsets.get(line).descendingIterator();
            while (it.hasNext()) {
                final int off = it.next();
                if (off < offsetInLine) {
                    return Optional.of(off);
                }
            }
            return Optional.empty();
        }
        
        private Optional<Integer> nextLineOffset(final int line, final int offsetInLine) {
            if (line >= this.offsets.size()) {
                return Optional.empty();
            }
            return this.offsets.get(line).stream().filter(o -> o > offsetInLine).findFirst();
        }
        
        public void moveDisplayDown(int lines) {
            final int height = Nano.this.size.getRows() - this.computeHeader().size() - Nano.this.computeFooter().size();
            while (--lines >= 0) {
                int lastLineToDisplay = this.firstLineToDisplay;
                if (!Nano.this.wrapping) {
                    lastLineToDisplay += height - 1;
                }
                else {
                    int off = this.offsetInLineToDisplay;
                    for (int l = 0; l < height - 1; ++l) {
                        final Optional<Integer> next = this.nextLineOffset(lastLineToDisplay, off);
                        if (next.isPresent()) {
                            off = next.get();
                        }
                        else {
                            off = 0;
                            ++lastLineToDisplay;
                        }
                    }
                }
                if (this.getLine(lastLineToDisplay) == null) {
                    this.eof();
                    return;
                }
                final Optional<Integer> next2 = this.nextLineOffset(this.firstLineToDisplay, this.offsetInLineToDisplay);
                if (next2.isPresent()) {
                    this.offsetInLineToDisplay = next2.get();
                }
                else {
                    this.offsetInLineToDisplay = 0;
                    ++this.firstLineToDisplay;
                }
            }
        }
        
        public void moveDisplayUp(int lines) {
            final int width = Nano.this.size.getColumns() - (Nano.this.printLineNumbers ? 8 : 0);
            while (--lines >= 0) {
                if (this.offsetInLineToDisplay > 0) {
                    this.offsetInLineToDisplay = Math.max(0, this.offsetInLineToDisplay - (width - 1));
                }
                else {
                    if (this.firstLineToDisplay <= 0) {
                        this.bof();
                        return;
                    }
                    --this.firstLineToDisplay;
                    this.offsetInLineToDisplay = this.prevLineOffset(this.firstLineToDisplay, Integer.MAX_VALUE).get();
                }
            }
        }
        
        private void cursorDown(int lines) {
            this.firstColumnToDisplay = 0;
            while (--lines >= 0) {
                if (!Nano.this.wrapping) {
                    if (this.getLine(this.line + 1) == null) {
                        this.bof();
                        break;
                    }
                    ++this.line;
                    this.offsetInLine = 0;
                    this.column = Math.min(this.length(this.getLine(this.line)), this.wantedColumn);
                }
                else {
                    String txt = this.getLine(this.line);
                    final Optional<Integer> off = this.nextLineOffset(this.line, this.offsetInLine);
                    if (off.isPresent()) {
                        this.offsetInLine = off.get();
                    }
                    else {
                        if (this.getLine(this.line + 1) == null) {
                            this.eof();
                            break;
                        }
                        ++this.line;
                        this.offsetInLine = 0;
                        txt = this.getLine(this.line);
                    }
                    final int next = this.nextLineOffset(this.line, this.offsetInLine).orElse(this.length(txt));
                    this.column = Math.min(this.wantedColumn, next - this.offsetInLine);
                }
            }
            this.moveToChar(this.offsetInLine + this.column);
        }
        
        private void cursorUp(int lines) {
            this.firstColumnToDisplay = 0;
            while (--lines >= 0) {
                if (!Nano.this.wrapping) {
                    if (this.line <= 0) {
                        this.bof();
                        break;
                    }
                    --this.line;
                    this.column = Math.min(this.length(this.getLine(this.line)) - this.offsetInLine, this.wantedColumn);
                }
                else {
                    final Optional<Integer> prev = this.prevLineOffset(this.line, this.offsetInLine);
                    if (prev.isPresent()) {
                        this.offsetInLine = prev.get();
                    }
                    else {
                        if (this.line <= 0) {
                            this.bof();
                            break;
                        }
                        --this.line;
                        this.offsetInLine = this.prevLineOffset(this.line, Integer.MAX_VALUE).get();
                        final int next = this.nextLineOffset(this.line, this.offsetInLine).orElse(this.length(this.getLine(this.line)));
                        this.column = Math.min(this.wantedColumn, next - this.offsetInLine);
                    }
                }
            }
            this.moveToChar(this.offsetInLine + this.column);
        }
        
        void ensureCursorVisible() {
            final List<AttributedString> header = this.computeHeader();
            final int rwidth = Nano.this.size.getColumns();
            final int height = Nano.this.size.getRows() - header.size() - Nano.this.computeFooter().size();
            while (this.line < this.firstLineToDisplay || (this.line == this.firstLineToDisplay && this.offsetInLine < this.offsetInLineToDisplay)) {
                this.moveDisplayUp(Nano.this.smoothScrolling ? 1 : (height / 2));
            }
            while (true) {
                final int cursor = this.computeCursorPosition(header.size() * Nano.this.size.getColumns() + (Nano.this.printLineNumbers ? 8 : 0), rwidth);
                if (cursor < (height + header.size()) * rwidth) {
                    break;
                }
                this.moveDisplayDown(Nano.this.smoothScrolling ? 1 : (height / 2));
            }
        }
        
        void eof() {
        }
        
        void bof() {
        }
        
        void resetDisplay() {
            this.moveRight(this.column += this.offsetInLine, true);
        }
        
        String getLine(final int line) {
            return (line < this.lines.size()) ? this.lines.get(line) : null;
        }
        
        String getTitle() {
            return (this.file != null) ? ("File: " + this.file) : "New Buffer";
        }
        
        List<AttributedString> computeHeader() {
            String left = Nano.this.getTitle();
            String middle = null;
            final String right = this.dirty ? "Modified" : "        ";
            final int width = Nano.this.size.getColumns();
            final int mstart = 2 + left.length() + 1;
            final int mend = width - 2 - 8;
            if (this.file == null) {
                middle = "New Buffer";
            }
            else {
                int max = mend - mstart;
                final String src = this.file;
                if ("File: ".length() + src.length() > max) {
                    final int lastSep = src.lastIndexOf(47);
                    if (lastSep > 0) {
                        final String p1 = src.substring(lastSep);
                        String p2;
                        for (p2 = src.substring(0, lastSep); p2.startsWith("."); p2 = p2.substring(1)) {}
                        final int nb = max - p1.length() - "File: ...".length();
                        final int cut = Math.max(0, Math.min(p2.length(), p2.length() - nb));
                        middle = "File: ..." + p2.substring(cut) + p1;
                    }
                    if (middle == null || middle.length() > max) {
                        left = null;
                        max = mend - 2;
                        final int nb2 = max - "File: ...".length();
                        final int cut2 = Math.max(0, Math.min(src.length(), src.length() - nb2));
                        middle = "File: ..." + src.substring(cut2);
                        if (middle.length() > max) {
                            middle = middle.substring(0, max);
                        }
                    }
                }
                else {
                    middle = "File: " + src;
                }
            }
            int pos = 0;
            final AttributedStringBuilder sb = new AttributedStringBuilder();
            sb.style(AttributedStyle.INVERSE);
            sb.append("  ");
            pos += 2;
            if (left != null) {
                sb.append(left);
                pos += left.length();
                sb.append(" ");
                ++pos;
                for (int i = 1; i < (Nano.this.size.getColumns() - middle.length()) / 2 - left.length() - 1 - 2; ++i) {
                    sb.append(" ");
                    ++pos;
                }
            }
            sb.append(middle);
            for (pos += middle.length(); pos < width - 8 - 2; ++pos) {
                sb.append(" ");
            }
            sb.append(right);
            sb.append("  \n");
            if (Nano.this.oneMoreLine) {
                return Collections.singletonList(sb.toAttributedString());
            }
            return Arrays.asList(sb.toAttributedString(), new AttributedString("\n"));
        }
        
        void highlightDisplayedLine(final int curLine, final int curOffset, final int nextOffset, final AttributedStringBuilder line) {
            final AttributedString disp = Nano.this.highlight ? this.syntaxHighlighter.highlight(new AttributedStringBuilder().tabs(Nano.this.tabs).append(this.getLine(curLine))) : new AttributedStringBuilder().tabs(Nano.this.tabs).append(this.getLine(curLine)).toAttributedString();
            final int[] hls = this.highlightStart();
            final int[] hle = this.highlightEnd();
            if (hls[0] == -1 || hle[0] == -1) {
                line.append(disp.columnSubSequence(curOffset, nextOffset));
            }
            else if (hls[0] == hle[0]) {
                if (curLine == hls[0]) {
                    if (hls[1] > nextOffset) {
                        line.append(disp.columnSubSequence(curOffset, nextOffset));
                    }
                    else if (hls[1] < curOffset) {
                        if (hle[1] > nextOffset) {
                            line.append(disp.columnSubSequence(curOffset, nextOffset), AttributedStyle.INVERSE);
                        }
                        else if (hle[1] > curOffset) {
                            line.append(disp.columnSubSequence(curOffset, hle[1]), AttributedStyle.INVERSE);
                            line.append(disp.columnSubSequence(hle[1], nextOffset));
                        }
                        else {
                            line.append(disp.columnSubSequence(curOffset, nextOffset));
                        }
                    }
                    else {
                        line.append(disp.columnSubSequence(curOffset, hls[1]));
                        if (hle[1] > nextOffset) {
                            line.append(disp.columnSubSequence(hls[1], nextOffset), AttributedStyle.INVERSE);
                        }
                        else {
                            line.append(disp.columnSubSequence(hls[1], hle[1]), AttributedStyle.INVERSE);
                            line.append(disp.columnSubSequence(hle[1], nextOffset));
                        }
                    }
                }
                else {
                    line.append(disp.columnSubSequence(curOffset, nextOffset));
                }
            }
            else if (curLine > hls[0] && curLine < hle[0]) {
                line.append(disp.columnSubSequence(curOffset, nextOffset), AttributedStyle.INVERSE);
            }
            else if (curLine == hls[0]) {
                if (hls[1] > nextOffset) {
                    line.append(disp.columnSubSequence(curOffset, nextOffset));
                }
                else if (hls[1] < curOffset) {
                    line.append(disp.columnSubSequence(curOffset, nextOffset), AttributedStyle.INVERSE);
                }
                else {
                    line.append(disp.columnSubSequence(curOffset, hls[1]));
                    line.append(disp.columnSubSequence(hls[1], nextOffset), AttributedStyle.INVERSE);
                }
            }
            else if (curLine == hle[0]) {
                if (hle[1] < curOffset) {
                    line.append(disp.columnSubSequence(curOffset, nextOffset));
                }
                else if (hle[1] > nextOffset) {
                    line.append(disp.columnSubSequence(curOffset, nextOffset), AttributedStyle.INVERSE);
                }
                else {
                    line.append(disp.columnSubSequence(curOffset, hle[1]), AttributedStyle.INVERSE);
                    line.append(disp.columnSubSequence(hle[1], nextOffset));
                }
            }
            else {
                line.append(disp.columnSubSequence(curOffset, nextOffset));
            }
        }
        
        List<AttributedString> getDisplayedLines(final int nbLines, final List<Diagnostic> diagnostics) {
            final AttributedStyle s = AttributedStyle.DEFAULT.foreground(8);
            final AttributedString cut = new AttributedString("\u2026", s);
            final AttributedString ret = new AttributedString("\u21a9", s);
            final List<AttributedString> newLines = new ArrayList<AttributedString>();
            final int rwidth = Nano.this.size.getColumns();
            final int width = rwidth - (Nano.this.printLineNumbers ? 8 : 0);
            int curLine = this.firstLineToDisplay;
            int curOffset = this.offsetInLineToDisplay;
            int prevLine = -1;
            if (Nano.this.highlight) {
                this.syntaxHighlighter.reset();
                for (int i = Math.max(0, curLine - nbLines); i < curLine; ++i) {
                    this.syntaxHighlighter.highlight(this.getLine(i));
                }
            }
            for (int terminalLine = 0; terminalLine < nbLines; ++terminalLine) {
                final AttributedStringBuilder line = new AttributedStringBuilder().tabs(Nano.this.tabs);
                if (Nano.this.printLineNumbers && curLine < this.lines.size()) {
                    line.style(s);
                    if (curLine != prevLine) {
                        line.append(String.format("%7d ", curLine + 1));
                    }
                    else {
                        line.append("      \u2027 ");
                    }
                    line.style(AttributedStyle.DEFAULT);
                    prevLine = curLine;
                }
                if (curLine < this.lines.size()) {
                    if (!Nano.this.wrapping) {
                        final AttributedString disp = new AttributedStringBuilder().tabs(Nano.this.tabs).append(this.getLine(curLine)).toAttributedString();
                        if (this.line == curLine) {
                            int cutCount = 1;
                            if (this.firstColumnToDisplay > 0) {
                                line.append(cut);
                                cutCount = 2;
                            }
                            if (disp.columnLength() - this.firstColumnToDisplay >= width - (cutCount - 1) * cut.columnLength()) {
                                this.highlightDisplayedLine(curLine, this.firstColumnToDisplay, this.firstColumnToDisplay + width - cutCount * cut.columnLength(), line);
                                line.append(cut);
                            }
                            else {
                                this.highlightDisplayedLine(curLine, this.firstColumnToDisplay, disp.columnLength(), line);
                            }
                        }
                        else if (disp.columnLength() >= width) {
                            this.highlightDisplayedLine(curLine, 0, width - cut.columnLength(), line);
                            line.append(cut);
                        }
                        else {
                            this.highlightDisplayedLine(curLine, 0, disp.columnLength(), line);
                        }
                        ++curLine;
                    }
                    else {
                        final Optional<Integer> nextOffset = this.nextLineOffset(curLine, curOffset);
                        if (nextOffset.isPresent()) {
                            this.highlightDisplayedLine(curLine, curOffset, nextOffset.get(), line);
                            line.append(ret);
                            curOffset = nextOffset.get();
                        }
                        else {
                            this.highlightDisplayedLine(curLine, curOffset, Integer.MAX_VALUE, line);
                            ++curLine;
                            curOffset = 0;
                        }
                    }
                }
                line.append('\n');
                newLines.add(line.toAttributedString());
            }
            if (diagnostics != null) {
                for (final Diagnostic diagnostic : diagnostics) {
                    if (diagnostic.getStartLine() == diagnostic.getEndLine()) {
                        final int line2 = diagnostic.getEndLine() - this.firstLineToDisplay;
                        final AttributedString attributedString = newLines.get(line2);
                        final AttributedStringBuilder builder = new AttributedStringBuilder(attributedString.length());
                        builder.append(attributedString.subSequence(0, diagnostic.getStartColumn()));
                        builder.append(attributedString.subSequence(diagnostic.getStartColumn(), diagnostic.getEndColumn()), AttributedStyle.DEFAULT.underline().foreground(1));
                        builder.append(attributedString.subSequence(diagnostic.getEndColumn(), attributedString.length()));
                        newLines.set(line2, builder.toAttributedString());
                        if (line2 != Nano.this.mouseY - 1 || Nano.this.mouseX < diagnostic.getStartColumn() || Nano.this.mouseX > diagnostic.getEndColumn()) {
                            continue;
                        }
                        final String message = diagnostic.getMessage();
                        if (message == null) {
                            continue;
                        }
                        if (message.isEmpty()) {
                            continue;
                        }
                        final int xi = diagnostic.getStartColumn();
                        final int dBoxSize = message.length() + 2;
                        final int maxWidth = (int)Math.round((Nano.this.size.getColumns() - xi) * 0.6);
                        final int xl = Math.min(dBoxSize + xi, xi + maxWidth);
                        final List<AttributedString> boxLines = Nano.this.adjustLines(Collections.singletonList(new AttributedString(message)), dBoxSize - 2, xl - xi - 2);
                        int yi = diagnostic.getStartLine() - this.firstLineToDisplay + 1;
                        int yl = yi + boxLines.size() + 1;
                        if (yl >= newLines.size()) {
                            yi = diagnostic.getStartLine() - this.firstLineToDisplay - boxLines.size() - 2;
                            yl = yi + boxLines.size() + 1;
                            if (yi < 0) {
                                continue;
                            }
                        }
                        final Box box = new Box(xi, yi, xl, yl);
                        box.setLines(boxLines);
                        box.draw(newLines);
                    }
                }
            }
            return newLines;
        }
        
        public void moveTo(int x, final int y) {
            if (Nano.this.printLineNumbers) {
                x = Math.max(x - 8, 0);
            }
            this.line = this.firstLineToDisplay;
            this.offsetInLine = this.offsetInLineToDisplay;
            this.wantedColumn = x;
            this.cursorDown(y);
        }
        
        public void gotoLine(int x, final int y) {
            this.line = ((y < this.lines.size()) ? y : (this.lines.size() - 1));
            x = Math.min(x, this.length(this.lines.get(this.line)));
            this.firstLineToDisplay = ((this.line > 0) ? (this.line - 1) : this.line);
            this.offsetInLine = 0;
            this.offsetInLineToDisplay = 0;
            this.column = 0;
            this.moveRight(x);
        }
        
        public int getDisplayedCursor() {
            return this.computeCursorPosition(Nano.this.printLineNumbers ? 8 : 0, Nano.this.size.getColumns() + 1);
        }
        
        private int computeCursorPosition(int cursor, final int rwidth) {
            int cur = this.firstLineToDisplay;
            int off = this.offsetInLineToDisplay;
            while (cur < this.line || off < this.offsetInLine) {
                if (!Nano.this.wrapping) {
                    cursor += rwidth;
                    ++cur;
                }
                else {
                    cursor += rwidth;
                    final Optional<Integer> next = this.nextLineOffset(cur, off);
                    if (next.isPresent()) {
                        off = next.get();
                    }
                    else {
                        ++cur;
                        off = 0;
                    }
                }
            }
            if (cur == this.line) {
                if (!Nano.this.wrapping && this.column > this.firstColumnToDisplay + this.width()) {
                    while (this.column > this.firstColumnToDisplay + this.width()) {
                        this.firstColumnToDisplay += this.width();
                    }
                }
                cursor += this.column - this.firstColumnToDisplay + ((this.firstColumnToDisplay > 0) ? 1 : 0);
                return cursor;
            }
            throw new IllegalStateException();
        }
        
        char getCurrentChar() {
            final String str = this.lines.get(this.line);
            if (this.column + this.offsetInLine < str.length()) {
                return str.charAt(this.column + this.offsetInLine);
            }
            if (this.line < this.lines.size() - 1) {
                return '\n';
            }
            return '\0';
        }
        
        public void prevWord() {
            while (Character.isAlphabetic(this.getCurrentChar()) && this.moveLeft(1)) {}
            while (!Character.isAlphabetic(this.getCurrentChar()) && this.moveLeft(1)) {}
            while (Character.isAlphabetic(this.getCurrentChar()) && this.moveLeft(1)) {}
            this.moveRight(1);
        }
        
        public void nextWord() {
            while (Character.isAlphabetic(this.getCurrentChar()) && this.moveRight(1)) {}
            while (!Character.isAlphabetic(this.getCurrentChar()) && this.moveRight(1)) {}
        }
        
        public void beginningOfLine() {
            final int n = 0;
            this.offsetInLine = n;
            this.column = n;
            this.wantedColumn = 0;
            this.ensureCursorVisible();
        }
        
        public void endOfLine() {
            final int x = this.length(this.lines.get(this.line));
            this.moveRight(x, true);
        }
        
        public void prevPage() {
            final int height = Nano.this.size.getRows() - this.computeHeader().size() - Nano.this.computeFooter().size();
            this.scrollUp(height - 2);
            this.column = 0;
            this.firstLineToDisplay = this.line;
            this.offsetInLineToDisplay = this.offsetInLine;
        }
        
        public void nextPage() {
            final int height = Nano.this.size.getRows() - this.computeHeader().size() - Nano.this.computeFooter().size();
            this.scrollDown(height - 2);
            this.column = 0;
            this.firstLineToDisplay = this.line;
            this.offsetInLineToDisplay = this.offsetInLine;
        }
        
        public void scrollUp(final int lines) {
            this.cursorUp(lines);
            this.moveDisplayUp(lines);
        }
        
        public void scrollDown(final int lines) {
            this.cursorDown(lines);
            this.moveDisplayDown(lines);
        }
        
        public void firstLine() {
            this.line = 0;
            final int n = 0;
            this.column = n;
            this.offsetInLine = n;
            this.ensureCursorVisible();
        }
        
        public void lastLine() {
            this.line = this.lines.size() - 1;
            final int n = 0;
            this.column = n;
            this.offsetInLine = n;
            this.ensureCursorVisible();
        }
        
        boolean nextSearch() {
            boolean out = false;
            if (Nano.this.searchTerm == null) {
                Nano.this.setMessage("No current search pattern");
                return false;
            }
            Nano.this.setMessage(null);
            int cur = this.line;
            final int dir = Nano.this.searchBackwards ? -1 : 1;
            int newPos = -1;
            int newLine = -1;
            final List<Integer> curRes = this.doSearch(this.lines.get(this.line));
            if (Nano.this.searchBackwards) {
                Collections.reverse(curRes);
            }
            for (final int r : curRes) {
                Label_0171: {
                    if (Nano.this.searchBackwards) {
                        if (r < this.offsetInLine + this.column) {
                            break Label_0171;
                        }
                        continue;
                    }
                    else {
                        if (r > this.offsetInLine + this.column) {
                            break Label_0171;
                        }
                        continue;
                    }
                    continue;
                }
                newPos = r;
                newLine = this.line;
                break;
            }
            if (newPos < 0) {
                while (true) {
                    cur = (cur + dir + this.lines.size()) % this.lines.size();
                    if (cur == this.line) {
                        break;
                    }
                    final List<Integer> res = this.doSearch(this.lines.get(cur));
                    if (!res.isEmpty()) {
                        newPos = (Nano.this.searchBackwards ? res.get(res.size() - 1) : res.get(0));
                        newLine = cur;
                        break;
                    }
                }
            }
            if (newPos < 0 && !curRes.isEmpty()) {
                newPos = curRes.get(0);
                newLine = this.line;
            }
            if (newPos >= 0) {
                if (newLine == this.line && newPos == this.offsetInLine + this.column) {
                    Nano.this.setMessage("This is the only occurence");
                    return false;
                }
                if ((Nano.this.searchBackwards && (newLine > this.line || (newLine == this.line && newPos > this.offsetInLine + this.column))) || (!Nano.this.searchBackwards && (newLine < this.line || (newLine == this.line && newPos < this.offsetInLine + this.column)))) {
                    Nano.this.setMessage("Search Wrapped");
                }
                this.line = newLine;
                this.moveRight(newPos, true);
                out = true;
            }
            else {
                Nano.this.setMessage("\"" + Nano.this.searchTerm + "\" not found");
            }
            return out;
        }
        
        private List<Integer> doSearch(final String text) {
            final Pattern pat = Pattern.compile(Nano.this.searchTerm, (Nano.this.searchCaseSensitive ? 0 : 66) | (Nano.this.searchRegexp ? 0 : 16));
            final Matcher m = pat.matcher(text);
            final List<Integer> res = new ArrayList<Integer>();
            while (m.find()) {
                res.add(m.start());
                Nano.this.matchedLength = m.group(0).length();
            }
            return res;
        }
        
        protected int[] highlightStart() {
            int[] out = { -1, -1 };
            if (Nano.this.mark) {
                out = this.getMarkStart();
            }
            else if (Nano.this.searchToReplace) {
                out[0] = this.line;
                out[1] = this.offsetInLine + this.column;
            }
            return out;
        }
        
        protected int[] highlightEnd() {
            int[] out = { -1, -1 };
            if (Nano.this.mark) {
                out = this.getMarkEnd();
            }
            else if (Nano.this.searchToReplace && Nano.this.matchedLength > 0) {
                out[0] = this.line;
                final int col = this.charPosition(this.offsetInLine + this.column) + Nano.this.matchedLength;
                if (col < this.lines.get(this.line).length()) {
                    out[1] = this.length(this.lines.get(this.line).substring(0, col));
                }
                else {
                    out[1] = this.length(this.lines.get(this.line));
                }
            }
            return out;
        }
        
        public void matching() {
            final int opening = this.getCurrentChar();
            final int idx = Nano.this.matchBrackets.indexOf(opening);
            if (idx < 0) {
                Nano.this.setMessage("Not a bracket");
                return;
            }
            final int dir = (idx >= Nano.this.matchBrackets.length() / 2) ? -1 : 1;
            final int closing = Nano.this.matchBrackets.charAt((idx + Nano.this.matchBrackets.length() / 2) % Nano.this.matchBrackets.length());
            int lvl = 1;
            int cur = this.line;
            int pos = this.offsetInLine + this.column;
            while (true) {
                if (pos + dir >= 0 && pos + dir < this.getLine(cur).length()) {
                    pos += dir;
                }
                else {
                    if (cur + dir < 0 || cur + dir >= this.lines.size()) {
                        Nano.this.setMessage("No matching bracket");
                        return;
                    }
                    cur += dir;
                    pos = ((dir > 0) ? 0 : (this.lines.get(cur).length() - 1));
                    if (pos < 0) {
                        continue;
                    }
                    if (pos >= this.lines.get(cur).length()) {
                        continue;
                    }
                }
                final int c = this.lines.get(cur).charAt(pos);
                if (c == opening) {
                    ++lvl;
                }
                else {
                    if (c == closing && --lvl == 0) {
                        this.line = cur;
                        this.moveToChar(pos);
                        this.ensureCursorVisible();
                        return;
                    }
                    continue;
                }
            }
        }
        
        private int length(final String line) {
            return new AttributedStringBuilder().tabs(Nano.this.tabs).append(line).columnLength();
        }
        
        void copy() {
            if (this.uncut || Nano.this.cut2end || Nano.this.mark) {
                Nano.this.cutbuffer = new ArrayList<String>();
            }
            if (Nano.this.mark) {
                final int[] s = this.getMarkStart();
                final int[] e = this.getMarkEnd();
                if (s[0] == e[0]) {
                    Nano.this.cutbuffer.add(this.lines.get(s[0]).substring(this.charPosition(s[0], s[1]), this.charPosition(e[0], e[1])));
                }
                else {
                    if (s[1] != 0) {
                        Nano.this.cutbuffer.add(this.lines.get(s[0]).substring(this.charPosition(s[0], s[1])));
                        ++s[0];
                    }
                    for (int i = s[0]; i < e[0]; ++i) {
                        Nano.this.cutbuffer.add(this.lines.get(i));
                    }
                    if (e[1] != 0) {
                        Nano.this.cutbuffer.add(this.lines.get(e[0]).substring(0, this.charPosition(e[0], e[1])));
                    }
                }
                Nano.this.mark = false;
                this.mark();
            }
            else if (Nano.this.cut2end) {
                final String l = this.lines.get(this.line);
                final int col = this.charPosition(this.offsetInLine + this.column);
                Nano.this.cutbuffer.add(l.substring(col));
                this.moveRight(l.substring(col).length());
            }
            else {
                Nano.this.cutbuffer.add(this.lines.get(this.line));
                this.cursorDown(1);
            }
            this.uncut = false;
        }
        
        void cut() {
            this.cut(false);
        }
        
        void cut(final boolean toEnd) {
            if (this.lines.size() > 1) {
                if (this.uncut || Nano.this.cut2end || toEnd || Nano.this.mark) {
                    Nano.this.cutbuffer = new ArrayList<String>();
                }
                if (Nano.this.mark) {
                    final int[] s = this.getMarkStart();
                    final int[] e = this.getMarkEnd();
                    if (s[0] == e[0]) {
                        final String l = this.lines.get(s[0]);
                        final int cols = this.charPosition(s[0], s[1]);
                        final int cole = this.charPosition(e[0], e[1]);
                        Nano.this.cutbuffer.add(l.substring(cols, cole));
                        this.lines.set(s[0], l.substring(0, cols) + l.substring(cole));
                        this.computeAllOffsets();
                        this.moveRight(cols, true);
                    }
                    else {
                        final int ls = s[0];
                        final int cs = this.charPosition(s[0], s[1]);
                        if (s[1] != 0) {
                            final String i = this.lines.get(s[0]);
                            Nano.this.cutbuffer.add(i.substring(cs));
                            this.lines.set(s[0], i.substring(0, cs));
                            ++s[0];
                        }
                        for (int j = s[0]; j < e[0]; ++j) {
                            Nano.this.cutbuffer.add(this.lines.get(s[0]));
                            this.lines.remove(s[0]);
                        }
                        if (e[1] != 0) {
                            final String i = this.lines.get(s[0]);
                            final int col = this.charPosition(e[0], e[1]);
                            Nano.this.cutbuffer.add(i.substring(0, col));
                            this.lines.set(s[0], i.substring(col));
                        }
                        this.computeAllOffsets();
                        this.gotoLine(cs, ls);
                    }
                    Nano.this.mark = false;
                    this.mark();
                }
                else if (Nano.this.cut2end || toEnd) {
                    final String k = this.lines.get(this.line);
                    final int col2 = this.charPosition(this.offsetInLine + this.column);
                    Nano.this.cutbuffer.add(k.substring(col2));
                    this.lines.set(this.line, k.substring(0, col2));
                    if (toEnd) {
                        ++this.line;
                        do {
                            Nano.this.cutbuffer.add(this.lines.get(this.line));
                            this.lines.remove(this.line);
                        } while (this.line <= this.lines.size() - 1);
                        --this.line;
                    }
                }
                else {
                    Nano.this.cutbuffer.add(this.lines.get(this.line));
                    this.lines.remove(this.line);
                    this.offsetInLine = 0;
                    if (this.line > this.lines.size() - 1) {
                        --this.line;
                    }
                }
                Nano.this.display.clear();
                this.computeAllOffsets();
                this.dirty = true;
                this.uncut = false;
            }
        }
        
        void uncut() {
            if (Nano.this.cutbuffer.isEmpty()) {
                return;
            }
            final String l = this.lines.get(this.line);
            final int col = this.charPosition(this.offsetInLine + this.column);
            if (Nano.this.cut2end) {
                this.lines.set(this.line, l.substring(0, col) + Nano.this.cutbuffer.get(0) + l.substring(col));
                this.computeAllOffsets();
                this.moveRight(col + Nano.this.cutbuffer.get(0).length(), true);
            }
            else if (col == 0) {
                this.lines.addAll(this.line, Nano.this.cutbuffer);
                this.computeAllOffsets();
                if (Nano.this.cutbuffer.size() > 1) {
                    this.gotoLine(Nano.this.cutbuffer.get(Nano.this.cutbuffer.size() - 1).length(), this.line + Nano.this.cutbuffer.size());
                }
                else {
                    this.moveRight(Nano.this.cutbuffer.get(0).length(), true);
                }
            }
            else {
                int gotol = this.line;
                if (Nano.this.cutbuffer.size() == 1) {
                    this.lines.set(this.line, l.substring(0, col) + Nano.this.cutbuffer.get(0) + l.substring(col));
                }
                else {
                    this.lines.set(this.line++, l.substring(0, col) + Nano.this.cutbuffer.get(0));
                    gotol = this.line;
                    this.lines.add(this.line, Nano.this.cutbuffer.get(Nano.this.cutbuffer.size() - 1) + l.substring(col));
                    for (int i = Nano.this.cutbuffer.size() - 2; i > 0; --i) {
                        ++gotol;
                        this.lines.add(this.line, Nano.this.cutbuffer.get(i));
                    }
                }
                this.computeAllOffsets();
                if (Nano.this.cutbuffer.size() > 1) {
                    this.gotoLine(Nano.this.cutbuffer.get(Nano.this.cutbuffer.size() - 1).length(), gotol);
                }
                else {
                    this.moveRight(col + Nano.this.cutbuffer.get(0).length(), true);
                }
            }
            Nano.this.display.clear();
            this.dirty = true;
            this.uncut = true;
        }
        
        void mark() {
            if (Nano.this.mark) {
                this.markPos[0] = this.line;
                this.markPos[1] = this.offsetInLine + this.column;
            }
            else {
                this.markPos[0] = -1;
                this.markPos[1] = -1;
            }
        }
        
        int[] getMarkStart() {
            int[] out = { -1, -1 };
            if (!Nano.this.mark) {
                return out;
            }
            if (this.markPos[0] > this.line || (this.markPos[0] == this.line && this.markPos[1] > this.offsetInLine + this.column)) {
                out[0] = this.line;
                out[1] = this.offsetInLine + this.column;
            }
            else {
                out = this.markPos;
            }
            return out;
        }
        
        int[] getMarkEnd() {
            int[] out = { -1, -1 };
            if (!Nano.this.mark) {
                return out;
            }
            if (this.markPos[0] > this.line || (this.markPos[0] == this.line && this.markPos[1] > this.offsetInLine + this.column)) {
                out = this.markPos;
            }
            else {
                out[0] = this.line;
                out[1] = this.offsetInLine + this.column;
            }
            return out;
        }
        
        void replaceFromCursor(final int chars, final String string) {
            final int pos = this.charPosition(this.offsetInLine + this.column);
            final String text = this.lines.get(this.line);
            String mod = text.substring(0, pos) + string;
            if (chars + pos < text.length()) {
                mod += text.substring(chars + pos);
            }
            this.lines.set(this.line, mod);
            this.dirty = true;
        }
    }
    
    protected static class PatternHistory
    {
        private final Path historyFile;
        private final int size = 100;
        private List<String> patterns;
        private int patternId;
        private boolean lastMoveUp;
        
        public PatternHistory(final Path historyFile) {
            this.patterns = new ArrayList<String>();
            this.patternId = -1;
            this.lastMoveUp = false;
            this.historyFile = historyFile;
            this.load();
        }
        
        public String up(final String hint) {
            String out = hint;
            if (!this.patterns.isEmpty() && this.patternId < this.patterns.size()) {
                if (!this.lastMoveUp && this.patternId > 0 && this.patternId < this.patterns.size() - 1) {
                    ++this.patternId;
                }
                if (this.patternId < 0) {
                    this.patternId = 0;
                }
                boolean found = false;
                for (int pid = this.patternId; pid < this.patterns.size(); ++pid) {
                    if (hint.isEmpty() || this.patterns.get(pid).startsWith(hint)) {
                        this.patternId = pid + 1;
                        out = this.patterns.get(pid);
                        found = true;
                        break;
                    }
                }
                if (!found) {
                    this.patternId = this.patterns.size();
                }
            }
            this.lastMoveUp = true;
            return out;
        }
        
        public String down(final String hint) {
            String out = hint;
            if (!this.patterns.isEmpty()) {
                if (this.lastMoveUp) {
                    --this.patternId;
                }
                if (this.patternId < 0) {
                    this.patternId = -1;
                }
                else {
                    boolean found = false;
                    for (int pid = this.patternId; pid >= 0; --pid) {
                        if (hint.isEmpty() || this.patterns.get(pid).startsWith(hint)) {
                            this.patternId = pid - 1;
                            out = this.patterns.get(pid);
                            found = true;
                            break;
                        }
                    }
                    if (!found) {
                        this.patternId = -1;
                    }
                }
            }
            this.lastMoveUp = false;
            return out;
        }
        
        public void add(final String pattern) {
            if (pattern.trim().isEmpty()) {
                return;
            }
            this.patterns.remove(pattern);
            if (this.patterns.size() > 100) {
                this.patterns.remove(this.patterns.size() - 1);
            }
            this.patterns.add(0, pattern);
            this.patternId = -1;
        }
        
        public void persist() {
            if (this.historyFile == null) {
                return;
            }
            try (final BufferedWriter writer = Files.newBufferedWriter(this.historyFile.toAbsolutePath(), StandardOpenOption.WRITE, StandardOpenOption.CREATE)) {
                for (final String s : this.patterns) {
                    if (!s.trim().isEmpty()) {
                        writer.append((CharSequence)s);
                        writer.newLine();
                    }
                }
            }
            catch (final Exception ex) {}
        }
        
        private void load() {
            if (this.historyFile == null) {
                return;
            }
            try {
                if (Files.exists(this.historyFile, new LinkOption[0])) {
                    this.patterns = new ArrayList<String>();
                    try (final BufferedReader reader = Files.newBufferedReader(this.historyFile)) {
                        reader.lines().forEach(line -> this.patterns.add(line));
                    }
                }
            }
            catch (final Exception ex) {}
        }
    }
    
    protected enum Operation
    {
        DO_LOWER_CASE, 
        QUIT, 
        WRITE, 
        READ, 
        GOTO, 
        FIND, 
        WRAP, 
        NUMBERS, 
        SMOOTH_SCROLLING, 
        MOUSE_SUPPORT, 
        ONE_MORE_LINE, 
        CLEAR_SCREEN, 
        UP, 
        DOWN, 
        LEFT, 
        RIGHT, 
        INSERT, 
        BACKSPACE, 
        NEXT_BUFFER, 
        PREV_BUFFER, 
        HELP, 
        NEXT_PAGE, 
        PREV_PAGE, 
        SCROLL_UP, 
        SCROLL_DOWN, 
        NEXT_WORD, 
        PREV_WORD, 
        LSP_SUGGESTION, 
        BEGINNING_OF_LINE, 
        END_OF_LINE, 
        FIRST_LINE, 
        LAST_LINE, 
        CUR_POS, 
        CASE_SENSITIVE, 
        BACKWARDS, 
        REGEXP, 
        ACCEPT, 
        CANCEL, 
        SEARCH, 
        TOGGLE_REPLACE, 
        MAC_FORMAT, 
        DOS_FORMAT, 
        APPEND_MODE, 
        PREPEND_MODE, 
        BACKUP, 
        TO_FILES, 
        YES, 
        NO, 
        ALL, 
        NEW_BUFFER, 
        EXECUTE, 
        NEXT_SEARCH, 
        MATCHING, 
        VERBATIM, 
        DELETE, 
        JUSTIFY_PARAGRAPH, 
        TO_SPELL, 
        CUT, 
        REPLACE, 
        MARK, 
        COPY, 
        INDENT, 
        UNINDENT, 
        BEGINNING_OF_PARAGRAPH, 
        END_OF_PARAGRAPH, 
        CUT_TO_END, 
        JUSTIFY_FILE, 
        COUNT, 
        CONSTANT_CURSOR, 
        WHITESPACE, 
        HIGHLIGHT, 
        SMART_HOME_KEY, 
        AUTO_INDENT, 
        CUT_TO_END_TOGGLE, 
        TABS_TO_SPACE, 
        UNCUT, 
        MOUSE_EVENT, 
        TOGGLE_SUSPENSION;
    }
    
    class Box
    {
        private final int xi;
        private final int xl;
        private final int yi;
        private final int yl;
        private List<AttributedString> lines;
        private int selected;
        private int selectedInView;
        private final int height;
        private AttributedStyle selectedStyle;
        private List<AttributedString> visibleLines;
        
        private Box(final int xi, final int yi, final int xl, final int yl) {
            this.selected = 0;
            this.selectedInView = 0;
            this.selectedStyle = AttributedStyle.DEFAULT;
            this.xi = xi;
            this.yi = yi;
            this.xl = xl;
            this.yl = yl;
            this.height = Math.max(1, yl - yi - 1);
        }
        
        private void setLines(final List<AttributedString> lines) {
            if (lines == null) {
                this.lines = Collections.emptyList();
                this.visibleLines = Collections.emptyList();
                return;
            }
            this.lines = lines;
            if (this.height > 0 && !lines.isEmpty()) {
                this.visibleLines = lines.subList(0, Math.min(this.height, lines.size()));
            }
            else {
                this.visibleLines = Collections.emptyList();
            }
        }
        
        public int getSelected() {
            return this.selected;
        }
        
        private void setSelectedStyle(final AttributedStyle selectedStyle) {
            this.selectedStyle = selectedStyle;
        }
        
        private AttributedStyle getSelectedStyle() {
            return this.selectedStyle;
        }
        
        private void down() {
            this.selected = Math.floorMod(this.selected + 1, this.lines.size());
            if (!this.scrollable() || this.selectedInView < this.height - 1) {
                ++this.selectedInView;
                return;
            }
            if (this.selected == 0) {
                this.selectedInView = 0;
                this.visibleLines = this.lines.subList(0, this.height);
            }
            else {
                this.visibleLines = this.lines.subList(this.selected - this.height + 1, this.selected + 1);
            }
        }
        
        private void up() {
            this.selected = Math.floorMod(this.selected - 1, this.lines.size());
            if (!this.scrollable() || this.selectedInView > 0) {
                --this.selectedInView;
                return;
            }
            if (this.selected == this.lines.size() - 1) {
                this.selectedInView = this.height - 1;
                this.visibleLines = this.lines.subList(this.lines.size() - this.height, this.lines.size());
            }
            else {
                this.visibleLines = this.lines.subList(this.selected, this.selected + this.height);
            }
        }
        
        private boolean scrollable() {
            return this.height < this.lines.size();
        }
        
        private int getSelectedInView() {
            return Math.floorMod(this.selectedInView, this.lines.size());
        }
        
        public void draw(final List<AttributedString> screenLines) {
            this.addBoxBorders(screenLines);
            this.addBoxLines(screenLines);
        }
        
        protected void addBoxBorders(final List<AttributedString> newLines) {
            if (newLines == null || newLines.isEmpty()) {
                return;
            }
            if (this.yi >= newLines.size() || this.yl >= newLines.size()) {
                return;
            }
            int width = this.xl - this.xi;
            if (width <= 0) {
                return;
            }
            if (width < 3) {
                width = 3;
            }
            if (this.yi >= 0) {
                final AttributedStringBuilder top = new AttributedStringBuilder(width);
                top.append('\u250c');
                top.append('\u2500', width - 2);
                top.append('\u2510');
                this.setLineInBox(newLines, this.yi, top.toAttributedString(), true);
            }
            final AttributedStringBuilder sides = new AttributedStringBuilder(width);
            sides.append('\u2502');
            sides.append(' ', width - 2);
            sides.append('\u2502');
            final AttributedString side = sides.toAttributedString();
            final int startY = Math.max(this.yi + 1, 0);
            for (int endY = Math.min(this.yl, newLines.size() - 1), y = startY; y < endY; ++y) {
                this.setLineInBox(newLines, y, side, true);
            }
            if (this.yl >= 0 && this.yl < newLines.size()) {
                final AttributedStringBuilder bottom = new AttributedStringBuilder(width);
                bottom.append('\u2514');
                bottom.append('\u2500', width - 2);
                bottom.append('\u2518');
                this.setLineInBox(newLines, this.yl, bottom.toAttributedString(), true);
            }
        }
        
        protected void setLineInBox(final List<AttributedString> newLines, final int y, final AttributedString line, final boolean borders) {
            if (y < 0 || y >= newLines.size()) {
                return;
            }
            int start = this.xi;
            int end = this.xl;
            if (!borders) {
                ++start;
                --end;
            }
            start = Math.max(0, start);
            end = Math.min(end, Nano.this.size.getColumns() - 1);
            final AttributedString currLine = newLines.get(y);
            final AttributedStringBuilder newLine = new AttributedStringBuilder(Math.max(end + 1, currLine.length() + 1));
            int currLength = currLine.length();
            boolean hasNewline = false;
            if (currLength > 0 && currLine.charAt(currLength - 1) == '\n') {
                --currLength;
                hasNewline = true;
            }
            newLine.append(currLine, 0, Math.min(start, currLength));
            if (start > currLength) {
                newLine.append(' ', start - currLength);
            }
            final int contentWidth = Math.min(line.length(), end - start);
            if (contentWidth > 0) {
                newLine.append(line, 0, contentWidth);
            }
            final int afterBoxStart = start + contentWidth;
            if (afterBoxStart < currLength) {
                newLine.append(currLine, afterBoxStart, currLength);
            }
            if (hasNewline) {
                newLine.append('\n');
            }
            newLines.set(y, newLine.toAttributedString());
        }
        
        protected void addBoxLines(final List<AttributedString> screenLines) {
            if (screenLines == null || screenLines.isEmpty() || this.visibleLines == null || this.visibleLines.isEmpty()) {
                return;
            }
            final int maxLines = this.yl - this.yi - 1;
            if (maxLines <= 0) {
                return;
            }
            for (int linesToDisplay = Math.min(this.visibleLines.size(), maxLines), i = 0; i < linesToDisplay; ++i) {
                final AttributedStringBuilder line = new AttributedStringBuilder(this.xl - this.xi - 2);
                AttributedStyle background = AttributedStyle.DEFAULT;
                if (i == this.getSelectedInView()) {
                    background = this.getSelectedStyle();
                }
                line.append(this.visibleLines.get(i), background);
                line.style(background);
                line.append(' ', this.xl - this.xi - line.length() - 2);
                final int lineY = this.yi + 1 + i;
                if (lineY < screenLines.size()) {
                    this.setLineInBox(screenLines, lineY, line.toAttributedString(), false);
                }
            }
        }
    }
    
    public interface Diagnostic
    {
        int getStartLine();
        
        int getStartColumn();
        
        int getEndLine();
        
        int getEndColumn();
        
        String getMessage();
    }
}
