From cf0f899327b898d37d2c8d2145ed78e2469da4f8 Mon Sep 17 00:00:00 2001 From: "Adam D. Ruppe" Date: Mon, 3 Feb 2020 17:44:48 -0500 Subject: [PATCH] fix stuff --- dom.d | 2 + terminal.d | 302 +++++++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 272 insertions(+), 32 deletions(-) diff --git a/dom.d b/dom.d index b3b20e8..5e9045b 100644 --- a/dom.d +++ b/dom.d @@ -5,6 +5,8 @@ // FIXME: the scriptable list is quite arbitrary +// FIXME: https://developer.mozilla.org/en-US/docs/Web/CSS/:is + // xml entity references?! diff --git a/terminal.d b/terminal.d index a8bbe1b..af56533 100644 --- a/terminal.d +++ b/terminal.d @@ -868,6 +868,14 @@ struct Terminal { } } + // EXPERIMENTAL do not use yet + Terminal alternateScreen() { + assert(this.type != ConsoleOutputType.cellular); + + this.flush(); + return Terminal(ConsoleOutputType.cellular); + } + version(Win32Console) { HANDLE hConsole; CONSOLE_SCREEN_BUFFER_INFO originalSbi; @@ -3064,7 +3072,7 @@ private int[] getTerminalCapabilities(int fdIn, int fdOut) { timeval tv; tv.tv_sec = 0; - tv.tv_usec = 10 * 1000; + tv.tv_usec = 100 * 1000; fd_set fs; FD_ZERO(&fs); @@ -3149,6 +3157,8 @@ private int[] getTerminalCapabilities(int fdIn, int fdOut) { return ret; } +private extern(C) int mkstemp(char *templ); + /** FIXME: support lines that wrap FIXME: better controls maybe @@ -3271,6 +3281,13 @@ class LineGetter { /// You might want to turn it off if generating a completion list is slow. bool autoSuggest = true; + /++ + Returns true if there was any input in the buffer. Can be + checked in the case of a [UserInterruptionException]. + +/ + bool hadInput() { + return line.length > 0; + } /// Override this if you don't want all lines added to the history. /// You can return null to not add it at all, or you can transform it. @@ -3290,9 +3307,17 @@ class LineGetter { file.writeln(item); } + /++ + History: + Introduced on January 31, 2020 + +/ + /* virtual */ string historyFileExtension() { + return ".history"; + } + private string historyPath() { import std.path; - auto filename = historyFileDirectory() ~ dirSeparator ~ historyFilename ~ ".history"; + auto filename = historyFileDirectory() ~ dirSeparator ~ historyFilename ~ historyFileExtension(); return filename; } @@ -3309,21 +3334,85 @@ class LineGetter { } } - /** + /++ Override this to provide tab completion. You may use the candidate argument to filter the list, but you don't have to (LineGetter will - do it for you on the values you return). + do it for you on the values you return). This means you can ignore + the arguments if you like. Ideally, you wouldn't return more than about ten items since the list gets difficult to use if it is too long. + Tab complete cannot modify text before or after the cursor at this time. + I *might* change that later to allow tab complete to fuzzy search and spell + check fix before. But right now it ONLY inserts. + Default is to provide recent command history as autocomplete. - */ - /* virtual */ protected string[] tabComplete(in dchar[] candidate) { + + Returns: + This function should return the full string to replace + `candidate[tabCompleteStartPoint(args) .. $]`. + For example, if your user wrote `wri` and you want to complete + it to `write` or `writeln`, you should return `["write", "writeln"]`. + + If you offer different tab complete in different places, you still + need to return the whole string. For example, a file competition of + a second argument, when the user writes `terminal.d term` and you + want it to complete to an additional `terminal.d`, you should return + `["terminal.d terminal.d"]`; in other words, `candidate ~ completion` + for each completion. + + It does this so you can simply return an array of words without having + to rebuild that array for each combination. + + To choose the word separator, override [tabCompleteStartPoint]. + + Params: + candidate = the text of the line up to the text cursor, after + which the completed text would be inserted + + afterCursor = the remaining text after the cursor. You can inspect + this, but cannot change it - this will be appended to the line + after completion, keeping the cursor in the same relative location. + + History: + Prior to January 30, 2020, this method took only one argument, + `candidate`. It now takes `afterCursor` as well, to allow you to + make more intelligent completions with full context. + +/ + /* virtual */ protected string[] tabComplete(in dchar[] candidate, in dchar[] afterCursor) { return history.length > 20 ? history[0 .. 20] : history; } - private string[] filterTabCompleteList(string[] list) { + /++ + Override this to provide a different tab competition starting point. The default + is `0`, always completing the complete line, but you may return the index of another + character of `candidate` to provide a new split. + + Returns: + The index of `candidate` where we should start the slice to keep in [tabComplete]. + It must be `>= 0 && <= candidate.length`. + + History: + Added on February 1, 2020. Initial default is to return 0 to maintain + old behavior. + +/ + /* virtual */ protected size_t tabCompleteStartPoint(in dchar[] candidate, in dchar[] afterCursor) { + return 0; + } + + /++ + This gives extra information for an item when displaying tab competition details. + + History: + Added January 31, 2020. + + +/ + /* virtual */ protected string tabCompleteHelp(string candidate) { + return null; + } + + private string[] filterTabCompleteList(string[] list, size_t start) { if(list.length == 0) return list; @@ -3332,28 +3421,47 @@ class LineGetter { foreach(item; list) { import std.algorithm; - if(startsWith(item, line[0 .. cursorPosition])) + if(startsWith(item, line[start .. cursorPosition])) f ~= item; } return f; } - /// Override this to provide a custom display of the tab completion list + /++ + Override this to provide a custom display of the tab completion list. + + History: + Prior to January 31, 2020, it only displayed the list. After + that, it would call [tabCompleteHelp] for each candidate and display + that string (if present) as well. + +/ protected void showTabCompleteList(string[] list) { if(list.length) { // FIXME: allow mouse clicking of an item, that would be cool + auto start = tabCompleteStartPoint(line[0 .. cursorPosition], line[cursorPosition .. $]); + // FIXME: scroll //if(terminal.type == ConsoleOutputType.linear) { terminal.writeln(); foreach(item; list) { terminal.color(suggestionForeground, background); import std.utf; - auto idx = codeLength!char(line[0 .. cursorPosition]); + auto idx = codeLength!char(line[start .. cursorPosition]); terminal.write(" ", item[0 .. idx]); terminal.color(regularForeground, background); - terminal.writeln(item[idx .. $]); + terminal.write(item[idx .. $]); + auto help = tabCompleteHelp(item); + if(help !is null) { + terminal.write("\t\t"); + int remaining = terminal.width - terminal.cursorX; + remaining -= 2; + if(remaining > 8) + terminal.write(remaining < help.length ? help[0 .. remaining] : help); + } + terminal.writeln(); + } updateCursorPosition(); redraw(); @@ -3361,6 +3469,84 @@ class LineGetter { } } + /++ + Called by the default event loop when the user presses F1. Override + `showHelp` to change the UI, override [helpMessage] if you just want + to change the message. + + History: + Introduced on January 30, 2020 + +/ + protected void showHelp() { + terminal.writeln(); + terminal.writeln(helpMessage); + updateCursorPosition(); + redraw(); + } + + /++ + History: + Introduced on January 30, 2020 + +/ + protected string helpMessage() { + return "Press F2 to edit current line in your editor. F3 searches. F9 runs current line while maintaining current edit state."; + } + + /++ + History: + Introduced on January 30, 2020 + +/ + protected dchar[] editLineInEditor(in dchar[] line, in size_t cursorPosition) { + import std.conv; + import std.process; + import std.file; + + char[] tmpName; + + version(Windows) { + import core.stdc.string; + char[280] path; + auto l = GetTempPathA(cast(DWORD) path.length, path.ptr); + if(l == 0) throw new Exception("GetTempPathA"); + path[l] = 0; + char[280] name; + auto r = GetTempFileNameA(path.ptr, "adr", 0, name.ptr); + if(r == 0) throw new Exception("GetTempFileNameA"); + tmpName = name[0 .. strlen(name.ptr)]; + scope(exit) + std.file.remove(tmpName); + std.file.write(tmpName, to!string(line)); + + string editor = environment.get("EDITOR", "notepad.exe"); + } else { + import core.stdc.stdlib; + import core.sys.posix.unistd; + char[120] name; + string p = "/tmp/adrXXXXXX"; + name[0 .. p.length] = p[]; + name[p.length] = 0; + auto fd = mkstemp(name.ptr); + tmpName = name[0 .. p.length]; + if(fd == -1) throw new Exception("mkstemp"); + scope(exit) + close(fd); + scope(exit) + std.file.remove(tmpName); + + string s = to!string(line); + while(s.length) { + auto x = write(fd, s.ptr, s.length); + if(x == -1) throw new Exception("write"); + s = s[x .. $]; + } + string editor = environment.get("EDITOR", "vi"); + } + + spawnProcess([editor, tmpName]).wait; + import std.string; + return to!(dchar[])(cast(char[]) std.file.read(tmpName)).chomp; + } + //private RealTimeConsoleInput* rtci; /// One-call shop for the main workhorse @@ -3447,9 +3633,11 @@ class LineGetter { private string suggestion(string[] list = null) { import std.algorithm, std.utf; auto relevantLineSection = line[0 .. cursorPosition]; + auto start = tabCompleteStartPoint(relevantLineSection, line[cursorPosition .. $]); + relevantLineSection = relevantLineSection[start .. $]; // FIXME: see about caching the list if we easily can if(list is null) - list = filterTabCompleteList(tabComplete(relevantLineSection)); + list = filterTabCompleteList(tabComplete(relevantLineSection, line[cursorPosition .. $]), start); if(list.length) { string commonality = list[0]; @@ -3607,15 +3795,19 @@ class LineGetter { /// function or else you might lose events or get exceptions from this. void startGettingLine() { // reset from any previous call first - cursorPosition = 0; - horizontalScrollPosition = 0; - justHitTab = false; - currentHistoryViewPosition = 0; - if(line.length) { - line = line[0 .. 0]; - line.assumeSafeAppend(); + if(!maintainBuffer) { + cursorPosition = 0; + horizontalScrollPosition = 0; + justHitTab = false; + currentHistoryViewPosition = 0; + if(line.length) { + line = line[0 .. 0]; + line.assumeSafeAppend(); + } } + maintainBuffer = false; + initializeWithSize(true); terminal.cursor = TerminalCursor.insert; @@ -3657,7 +3849,7 @@ class LineGetter { positionCursor(); } - lastDrawLength = terminal.width; + lastDrawLength = terminal.width - terminal.cursorX; version(Win32Console) lastDrawLength -= 1; // I don't like this but Windows resizing is different anyway and it is liable to scroll if i go over.. @@ -3767,6 +3959,13 @@ class LineGetter { return s; } + void showIndividualHelp(string help) { + terminal.writeln(); + terminal.writeln(help); + } + + private bool maintainBuffer; + /// for integrating into another event loop /// you can pass individual events to this and /// the line getter will work on it @@ -3799,7 +3998,9 @@ class LineGetter { return false; case '\t': auto relevantLineSection = line[0 .. cursorPosition]; - auto possibilities = filterTabCompleteList(tabComplete(relevantLineSection)); + auto start = tabCompleteStartPoint(relevantLineSection, line[cursorPosition .. $]); + relevantLineSection = relevantLineSection[start .. $]; + auto possibilities = filterTabCompleteList(tabComplete(relevantLineSection, line[cursorPosition .. $]), start); import std.utf; if(possibilities.length == 1) { @@ -3807,6 +4008,13 @@ class LineGetter { if(toFill.length) { addString(toFill); redraw(); + } else { + auto help = this.tabCompleteHelp(possibilities[0]); + if(help.length) { + showIndividualHelp(help); + updateCursorPosition(); + redraw(); + } } justHitTab = false; } else { @@ -3843,6 +4051,38 @@ class LineGetter { redraw(); } break; + case KeyboardEvent.Key.F1: + justHitTab = false; + showHelp(); + break; + case KeyboardEvent.Key.F2: + justHitTab = false; + line = editLineInEditor(line, cursorPosition); + if(cursorPosition > line.length) + cursorPosition = cast(int) line.length; + positionCursor(); + redraw(); + break; + case KeyboardEvent.Key.F3: + // case 'r' - 'a' + 1: // ctrl+r + justHitTab = false; + // search in history + // FIXME: what about search in completion too? + break; + case KeyboardEvent.Key.F4: + justHitTab = false; + // FIXME: clear line + break; + case KeyboardEvent.Key.F9: + justHitTab = false; + // compile and run analog; return the current string + // but keep the buffer the same + maintainBuffer = true; + return false; + case 0x1d: // ctrl+5, because of vim % shortcut + justHitTab = false; + // FIXME: find matching delimiter + break; case KeyboardEvent.Key.LeftArrow: justHitTab = false; if(cursorPosition) @@ -4033,21 +4273,19 @@ class FileLineGetter : LineGetter { /// to complete. string searchDirectory = "."; - override protected string[] tabComplete(in dchar[] candidate) { + override size_t tabCompleteStartPoint(in dchar[] candidate, in dchar[] afterCursor) { + import std.string; + return candidate.lastIndexOf(" ") + 1; + } + + override protected string[] tabComplete(in dchar[] candidate, in dchar[] afterCursor) { import std.file, std.conv, std.algorithm, std.string; - const(dchar)[] soFar = candidate; - auto idx = candidate.lastIndexOf(" "); - if(idx != -1) - soFar = candidate[idx + 1 .. $]; string[] list; foreach(string name; dirEntries(searchDirectory, SpanMode.breadth)) { - // try without the ./ - if(startsWith(name[2..$], soFar)) - list ~= text(candidate, name[searchDirectory.length + 1 + soFar.length .. $]); - else // and with - if(startsWith(name, soFar)) - list ~= text(candidate, name[soFar.length .. $]); + // both with and without the (searchDirectory ~ "/") + list ~= name[searchDirectory.length + 1 .. $]; + list ~= name[0 .. $]; } return list;