libcmd|rs: use rustyline instead of editline for the repl
Change-Id: Ic414de3cc2435d9615305f05a85aa9c923e82a69
This commit is contained in:
Generated
+126
-1
@@ -82,7 +82,7 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72fe02fc62033df9ba41cba57ee19acf5e742511a140c7dbc3a873e19a19a1bd"
|
||||
dependencies = [
|
||||
"unicode-width",
|
||||
"unicode-width 0.1.14",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
@@ -145,6 +145,12 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@@ -170,6 +176,12 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chumsky"
|
||||
version = "1.0.0-alpha.8"
|
||||
@@ -224,6 +236,15 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "5.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
|
||||
dependencies = [
|
||||
"error-code",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
@@ -277,12 +298,24 @@ version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
||||
|
||||
[[package]]
|
||||
name = "endian-type"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "869b0adbda23651a9c5c0c3d270aac9fcb52e8622a8f2b17e57802d7791962f2"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "error-code"
|
||||
version = "3.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
|
||||
|
||||
[[package]]
|
||||
name = "expect-test"
|
||||
version = "1.5.0"
|
||||
@@ -350,6 +383,15 @@ version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "home"
|
||||
version = "0.5.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humansize"
|
||||
version = "2.1.3"
|
||||
@@ -409,6 +451,8 @@ name = "lix"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"lix-doc",
|
||||
"rustyline",
|
||||
"rustyline-derive",
|
||||
"zngur",
|
||||
]
|
||||
|
||||
@@ -421,6 +465,12 @@ dependencies = [
|
||||
"rowan",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.2"
|
||||
@@ -449,6 +499,27 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "nibble_vec"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
@@ -523,6 +594,16 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "radix_trie"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b4431027dcd37fc2a73ef740b5f233aa805897935b8bce0195e41bbf9a3289a"
|
||||
dependencies = [
|
||||
"endian-type",
|
||||
"nibble_vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.3.9"
|
||||
@@ -567,6 +648,38 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustyline"
|
||||
version = "18.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53f6a737db68eb1a8ccff86b584b2fc13eca6a7bb6f78ebc7c529547e3ab9684"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"clipboard-win",
|
||||
"home",
|
||||
"libc",
|
||||
"log",
|
||||
"memchr",
|
||||
"nix",
|
||||
"radix_trie",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.2.2",
|
||||
"utf8parse",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustyline-derive"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64e5587417a3c4e16a4415e8d7d07f80998ed835ade621d19dfbe9fbe3205b0f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
@@ -614,6 +727,12 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
|
||||
[[package]]
|
||||
name = "stacker"
|
||||
version = "0.1.24"
|
||||
@@ -680,6 +799,12 @@ version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
|
||||
@@ -11,6 +11,8 @@ edition = "2021"
|
||||
|
||||
[workspace.dependencies]
|
||||
clap = "4"
|
||||
rustyline = "18"
|
||||
rustyline-derive = "0.12"
|
||||
syn = "2.0"
|
||||
zngur = "0.10"
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
synopsis: "REPL now uses rustyline"
|
||||
cls: [5703]
|
||||
category: "Improvements"
|
||||
credits: [horrors]
|
||||
issues: []
|
||||
---
|
||||
|
||||
The REPL now uses [rustyline](https://github.com/kkawakam/rustyline) for input processing instead
|
||||
of editline. This comes with some improvements to REPL behavior: wrapping lines no longer confuse
|
||||
the line editor, unicode is fully supported, pasting multiline expressions is noew possible, even
|
||||
undo commands are now available! We plan to improve the REPL further using these newfound powers.
|
||||
@@ -50,10 +50,6 @@ The most current alternative to this section is to read `package.nix` and see wh
|
||||
- The `boost` library of version 1.66.0 or higher. It can be obtained
|
||||
from the official web site <https://www.boost.org/>.
|
||||
|
||||
- The `editline` library of version 1.14.0 or higher. It can be
|
||||
obtained from the its repository
|
||||
<https://github.com/troglobit/editline>.
|
||||
|
||||
- Recent versions of Bison and Flex to build the parser. (This is
|
||||
because Nix needs GLR support in Bison and reentrancy support in
|
||||
Flex.) For Bison, you need version 2.6, which can be obtained from
|
||||
|
||||
+57
-216
@@ -1,251 +1,97 @@
|
||||
#include "lix/libutil/c-calls.hh"
|
||||
#include "libutil/fmt.hh"
|
||||
#include "lix/libutil/error.hh"
|
||||
#include "lix/libutil/file-system.hh"
|
||||
#include "lix/libutil/logging.hh"
|
||||
#include <csignal>
|
||||
#include <cstdio>
|
||||
#include "lix/lix-rs/main.gen.hh"
|
||||
#include "lix/lix-rs/utils.hh"
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <cerrno>
|
||||
|
||||
// editline < 1.15.2 don't wrap their API for C++ usage
|
||||
// (added in https://github.com/troglobit/editline/commit/91398ceb3427b730995357e9d120539fb9bb7461).
|
||||
// This results in linker errors due to to name-mangling of editline C symbols.
|
||||
// For compatibility with these versions, we wrap the API here
|
||||
// (wrapping multiple times on newer versions is no problem).
|
||||
extern "C" {
|
||||
#include <editline.h>
|
||||
}
|
||||
|
||||
#include "lix/libutil/finally.hh"
|
||||
#include "lix/libutil/strings.hh"
|
||||
#include "lix/libcmd/repl-interacter.hh"
|
||||
|
||||
namespace rust {
|
||||
Vec<String> Impl<lix::repl::CxxCompleter, lix::repl::ReplCompleter>::complete(
|
||||
Ref<lix::repl::CxxCompleter> self, Ref<Str> input
|
||||
)
|
||||
try {
|
||||
auto s = to_std_string(input);
|
||||
|
||||
auto result = std::vec::Vec<std::string::String>::new_();
|
||||
for (auto & possible : self.cpp().completePrefix(s)) {
|
||||
result.push(rust::to_string(possible));
|
||||
}
|
||||
return result;
|
||||
} catch (...) {
|
||||
// the completer should have logged anything interesting.
|
||||
return std::vec::Vec<std::string::String>::new_();
|
||||
}
|
||||
}
|
||||
|
||||
namespace nix {
|
||||
|
||||
namespace {
|
||||
// Used to communicate to NixRepl::getLine whether a signal occurred in ::readline.
|
||||
volatile sig_atomic_t g_signal_received = 0;
|
||||
ReadlineLikeInteracter::ReadlineLikeInteracter(std::string historyFile) : historyFile(historyFile) {}
|
||||
|
||||
void sigintHandler(int signo)
|
||||
void ReadlineLikeInteracter::init(detail::ReplCompleterMixin * repl)
|
||||
{
|
||||
g_signal_received = signo;
|
||||
}
|
||||
|
||||
static detail::ReplCompleterMixin * curRepl; // ugly
|
||||
|
||||
/**
|
||||
* @return a null-terminated list of completions as expected by `el_print_columns`
|
||||
*/
|
||||
char ** copyCompletions(const StringSet& possible)
|
||||
{
|
||||
int ac = 0;
|
||||
char ** vp = nullptr;
|
||||
|
||||
auto check = [&](auto * p) {
|
||||
if (!p) {
|
||||
if (vp) {
|
||||
while (--ac >= 0)
|
||||
free(vp[ac]);
|
||||
free(static_cast<void *>(vp));
|
||||
}
|
||||
throw Error("allocation failure");
|
||||
}
|
||||
return p;
|
||||
};
|
||||
vp = check(static_cast<char **>(malloc(possible.size() * sizeof(char *))));
|
||||
for (auto & p : possible)
|
||||
vp[ac++] = check(strdup(p.c_str()));
|
||||
|
||||
return vp;
|
||||
}
|
||||
|
||||
// Instead of using the readline-provided prefix, do our own tokenization
|
||||
// to avoid the default behavior of treating dots/quotes as word boundaries.
|
||||
// See the definition of SEPS for what it treats as a boundary:
|
||||
// https://github.com/troglobit/editline/blob/caf4b3c0ce3b0785791198b11de6f3134e9f05d8/src/editline.c
|
||||
std::string getLastTokenBeforeCursor()
|
||||
{
|
||||
std::string_view line{rl_line_buffer, static_cast<size_t>(rl_point)};
|
||||
|
||||
auto tokens = tokenizeString<std::vector<std::string>>(
|
||||
line,
|
||||
// Same as editline's SEPS, except for double and single quotes:
|
||||
"#$&()*:;<=>?[\\]^`{|}~\n\t "
|
||||
);
|
||||
|
||||
if (tokens.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return tokens.back();
|
||||
}
|
||||
|
||||
// Sometimes inserting text or listing possible completions has a side effect
|
||||
// of hiding the text after the cursor (even though it remains in the buffer).
|
||||
// This helper just refreshes the display while keeping the cursor in place.
|
||||
//
|
||||
// Inserting text also sometimes moves the whole buffer down one line, usually
|
||||
// if the cursor is inside a quoted attr name. I'm not sure why (vs unquoted)
|
||||
// but it still seems to work pretty well and is just a visual artifact.
|
||||
el_status_t redisplay()
|
||||
{
|
||||
int cursorPos = rl_point;
|
||||
rl_refresh_line(0, 0);
|
||||
rl_point = cursorPos;
|
||||
return (rl_point == rl_end) ? CSstay : CSmove;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
static el_status_t doCompletion() {
|
||||
auto s = getLastTokenBeforeCursor();
|
||||
auto possible = curRepl->completePrefix(s);
|
||||
|
||||
if (possible.empty()) {
|
||||
return el_ring_bell();
|
||||
}
|
||||
|
||||
if (possible.size() == 1) {
|
||||
const auto completion = *possible.cbegin();
|
||||
if (completion.size() > s.size()) {
|
||||
rl_insert_text(requireCString(completion.substr(s.size())));
|
||||
return redisplay();
|
||||
}
|
||||
|
||||
return el_ring_bell();
|
||||
}
|
||||
|
||||
auto checkAllHaveSameAt = [&](size_t pos) {
|
||||
auto & first = *possible.begin();
|
||||
for (auto & p : possible) {
|
||||
if (p.size() <= pos || p[pos] != first[pos]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
size_t start = s.size();
|
||||
size_t len = 0;
|
||||
while (checkAllHaveSameAt(start + len)) {
|
||||
++len;
|
||||
}
|
||||
if (len > 0) {
|
||||
auto commonPrefix = possible.begin()->substr(start, len);
|
||||
rl_insert_text(requireCString(commonPrefix));
|
||||
el_ring_bell();
|
||||
return redisplay();
|
||||
}
|
||||
|
||||
char** columns = copyCompletions(possible);
|
||||
el_print_columns(possible.size(), columns);
|
||||
return redisplay();
|
||||
}
|
||||
|
||||
ReadlineLikeInteracter::Guard ReadlineLikeInteracter::init(detail::ReplCompleterMixin * repl)
|
||||
{
|
||||
// Allow nix-repl specific settings in .inputrc
|
||||
rl_readline_name = "nix-repl";
|
||||
try {
|
||||
createDirs(dirOf(historyFile));
|
||||
} catch (SysError & e) {
|
||||
logWarning(e.info());
|
||||
}
|
||||
el_hist_size = 1000;
|
||||
read_history(requireCString(historyFile));
|
||||
auto oldRepl = curRepl;
|
||||
curRepl = repl;
|
||||
Guard restoreRepl([oldRepl] { curRepl = oldRepl; });
|
||||
// editline does its own escaping of completions, so we rebind tab
|
||||
// to our own completion function to skip that and do nix escaping
|
||||
// instead of shell escaping.
|
||||
el_bind_key(CTL('I'), doCompletion);
|
||||
return restoreRepl;
|
||||
|
||||
auto rl = repl::Rustyline::new_(rust::to_string(historyFile).as_str(), *repl);
|
||||
match_result(
|
||||
std::move(rl),
|
||||
[&](repl::Rustyline ok) { this->rl = std::make_unique<repl::Rustyline>(std::move(ok)); },
|
||||
[&](rust::Box<rust::Dyn<rust::std::error::Error>> err) {
|
||||
throw Error("%s", Uncolored(to_std_string(err.to_string())));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static constexpr const char * promptForType(ReplPromptType promptType)
|
||||
static rust::Ref<rust::Str> promptForType(ReplPromptType promptType)
|
||||
{
|
||||
switch (promptType) {
|
||||
case ReplPromptType::ReplPrompt:
|
||||
return "nix-repl> ";
|
||||
return "nix-repl> "_rs;
|
||||
case ReplPromptType::ContinuationPrompt:
|
||||
return " ";
|
||||
return " "_rs;
|
||||
}
|
||||
assert(false);
|
||||
}
|
||||
|
||||
bool ReadlineLikeInteracter::getLine(std::string & input, ReplPromptType promptType)
|
||||
{
|
||||
struct sigaction act, old;
|
||||
sigset_t savedSignalMask, set;
|
||||
auto s = rl->ask(promptForType(promptType));
|
||||
|
||||
auto setupSignals = [&]() {
|
||||
act.sa_handler = sigintHandler;
|
||||
sigfillset(&act.sa_mask);
|
||||
act.sa_flags = 0;
|
||||
if (sigaction(SIGINT, &act, &old))
|
||||
throw SysError("installing handler for SIGINT");
|
||||
return match_result(
|
||||
std::move(s),
|
||||
[&](rust::String ok) {
|
||||
input += to_std_string(ok);
|
||||
input += '\n';
|
||||
return true;
|
||||
},
|
||||
[&](rust::rustyline::error::ReadlineError err) {
|
||||
if (err.matches_Interrupted()) {
|
||||
input.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
sigemptyset(&set);
|
||||
sigaddset(&set, SIGINT);
|
||||
if (sigprocmask(SIG_UNBLOCK, &set, &savedSignalMask))
|
||||
throw SysError("unblocking SIGINT");
|
||||
};
|
||||
auto restoreSignals = [&]() {
|
||||
if (sigprocmask(SIG_SETMASK, &savedSignalMask, nullptr))
|
||||
throw SysError("restoring signals");
|
||||
if (err.matches_Eof()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (sigaction(SIGINT, &old, 0))
|
||||
throw SysError("restoring handler for SIGINT");
|
||||
};
|
||||
|
||||
setupSignals();
|
||||
char * s = readline(promptForType(promptType)); // NOLINT(lix-unsafe-c-calls)
|
||||
Finally doFree([&]() { free(s); });
|
||||
restoreSignals();
|
||||
|
||||
if (g_signal_received) {
|
||||
g_signal_received = 0;
|
||||
input.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!s)
|
||||
return false;
|
||||
|
||||
this->writeHistory();
|
||||
input += s;
|
||||
input += '\n';
|
||||
return true;
|
||||
throw Error("%s", Uncolored(to_std_string(err.into().to_string())));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void ReadlineLikeInteracter::writeHistory()
|
||||
{
|
||||
int ret = write_history(requireCString(historyFile));
|
||||
int writeHistErr = errno;
|
||||
|
||||
if (ret == 0) {
|
||||
return;
|
||||
if (rl) {
|
||||
rl->write_history();
|
||||
}
|
||||
|
||||
// If the open fails, editline returns EOF. If the close fails, editline
|
||||
// forwards the return value of fclose(), which is EOF on error.
|
||||
// readline however, returns the errno.
|
||||
// So if we didn't get exactly EOF, then consider the return value the error
|
||||
// code; otherwise use the errno we saved above.
|
||||
// https://github.com/troglobit/editline/issues/66
|
||||
if (ret != EOF) {
|
||||
writeHistErr = ret;
|
||||
}
|
||||
|
||||
// In any of these cases, we should explicitly ignore the error, but log
|
||||
// them so the user isn't confused why their history is getting eaten.
|
||||
|
||||
std::string_view const errMsg(std::strerror(writeHistErr));
|
||||
printTaggedWarning("ignoring error writing repl history to %s: %s", this->historyFile, errMsg);
|
||||
}
|
||||
|
||||
ReadlineLikeInteracter::~ReadlineLikeInteracter()
|
||||
@@ -253,11 +99,6 @@ ReadlineLikeInteracter::~ReadlineLikeInteracter()
|
||||
this->writeHistory();
|
||||
}
|
||||
|
||||
AutomationInteracter::Guard AutomationInteracter::init(detail::ReplCompleterMixin *)
|
||||
{
|
||||
return Guard([] {});
|
||||
}
|
||||
|
||||
// ASCII ENQ character
|
||||
constexpr const char * automationPrompt = "\x05";
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
#pragma once
|
||||
/// @file
|
||||
|
||||
#include "lix/libutil/finally.hh"
|
||||
#include "lix/libutil/types.hh"
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace rust::lix::repl {
|
||||
struct Rustyline;
|
||||
}
|
||||
|
||||
namespace nix {
|
||||
|
||||
namespace detail {
|
||||
@@ -24,9 +27,7 @@ enum class ReplPromptType {
|
||||
class ReplInteracter
|
||||
{
|
||||
public:
|
||||
using Guard = Finally<std::function<void()>>;
|
||||
|
||||
virtual Guard init(detail::ReplCompleterMixin * repl) = 0;
|
||||
virtual void init(detail::ReplCompleterMixin * repl) {}
|
||||
/** Returns a boolean of whether the interacter got EOF */
|
||||
virtual bool getLine(std::string & input, ReplPromptType promptType) = 0;
|
||||
virtual ~ReplInteracter(){};
|
||||
@@ -35,12 +36,12 @@ public:
|
||||
class ReadlineLikeInteracter final : public ReplInteracter
|
||||
{
|
||||
std::string historyFile;
|
||||
std::unique_ptr<rust::lix::repl::Rustyline> rl;
|
||||
|
||||
public:
|
||||
ReadlineLikeInteracter(std::string historyFile)
|
||||
: historyFile(historyFile)
|
||||
{
|
||||
}
|
||||
virtual Guard init(detail::ReplCompleterMixin * repl) override;
|
||||
ReadlineLikeInteracter(std::string historyFile);
|
||||
|
||||
virtual void init(detail::ReplCompleterMixin * repl) override;
|
||||
virtual bool getLine(std::string & input, ReplPromptType promptType) override;
|
||||
/** Writes the current history to the history file.
|
||||
*
|
||||
@@ -54,7 +55,6 @@ class AutomationInteracter final : public ReplInteracter
|
||||
{
|
||||
public:
|
||||
AutomationInteracter() = default;
|
||||
virtual Guard init(detail::ReplCompleterMixin * repl) override;
|
||||
virtual bool getLine(std::string & input, ReplPromptType promptType) override;
|
||||
virtual ~AutomationInteracter() override = default;
|
||||
};
|
||||
|
||||
+4
-10
@@ -1,6 +1,5 @@
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <editline.h>
|
||||
#include <iostream>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -380,7 +379,7 @@ ReplExitStatus NixRepl::mainLoop()
|
||||
std::list<ReplLoadable> loadables = std::exchange(loaded, {});
|
||||
loadFiles(loadables);
|
||||
|
||||
auto _guard = interacter->init(static_cast<detail::ReplCompleterMixin *>(this));
|
||||
interacter->init(static_cast<detail::ReplCompleterMixin *>(this));
|
||||
|
||||
/* Stop the progress bar because it interferes with the display of
|
||||
the repl. */
|
||||
@@ -441,15 +440,10 @@ StringSet NixRepl::completePrefix(const std::string &prefix)
|
||||
{
|
||||
StringSet completions;
|
||||
|
||||
// We should only complete colon commands if there's a colon at the beginning,
|
||||
// but editline (for... whatever reason) doesn't *give* us the colon in the
|
||||
// completion callback. If the user types :rel<TAB>, `prefix` will only be `rel`.
|
||||
// Luckily, editline provides a global variable for its current buffer, so we can
|
||||
// check for the presence of a colon there.
|
||||
if (rl_line_buffer != nullptr && rl_line_buffer[0] == ':') {
|
||||
if (prefix.starts_with(':')) {
|
||||
for (auto const & [colonCmd, cmd] : registeredCommands) {
|
||||
if ((!cmd->attributes.debugModeOnly || inDebugger()) && colonCmd.starts_with(prefix)) {
|
||||
completions.insert(std::string(colonCmd));
|
||||
if ((!cmd->attributes.debugModeOnly || inDebugger()) && colonCmd.starts_with(prefix.substr(1))) {
|
||||
completions.insert(":" + colonCmd);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,4 +2,4 @@
|
||||
name: repl-automation
|
||||
internalName: ReplAutomation
|
||||
---
|
||||
Makes the repl not use editline, print ENQ (U+0005) when ready for a command, and take commands followed by newline.
|
||||
Makes the repl not use a line editor, print ENQ (U+0005) when ready for a command, and take commands followed by newline.
|
||||
|
||||
@@ -12,6 +12,8 @@ default = []
|
||||
|
||||
[dependencies]
|
||||
lix-doc.path = "../lix-doc"
|
||||
rustyline.workspace = true
|
||||
rustyline-derive.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
zngur.workspace = true
|
||||
|
||||
@@ -3,6 +3,7 @@ lix_rs_files += files(
|
||||
# keep-sorted start
|
||||
'src/ffi_test.rs',
|
||||
'src/lib.rs',
|
||||
'src/repl.rs',
|
||||
# keep-sorted end
|
||||
)
|
||||
|
||||
|
||||
@@ -146,3 +146,4 @@ pub(crate) mod log {
|
||||
}
|
||||
|
||||
mod ffi_test;
|
||||
mod repl;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
merge "./ffi_test.zng";
|
||||
merge "./repl.zng";
|
||||
|
||||
mod crate::ffi {
|
||||
use ::std::result::Result as Result;
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
use std::{
|
||||
error::Error,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use rustyline::{
|
||||
completion::{extract_word, Completer},
|
||||
error::ReadlineError,
|
||||
history::FileHistory,
|
||||
CompletionType, Config, Editor,
|
||||
};
|
||||
|
||||
use rustyline_derive::*;
|
||||
|
||||
pub(crate) use crate::generated::cpp::CxxCompleter;
|
||||
|
||||
pub(crate) trait ReplCompleter {
|
||||
fn complete(&self, input: &str) -> Vec<String>;
|
||||
}
|
||||
|
||||
#[derive(Helper, Validator, Hinter, Highlighter)]
|
||||
struct Helper<'a> {
|
||||
cxx: &'a CxxCompleter,
|
||||
}
|
||||
|
||||
impl<'a> Completer for Helper<'a> {
|
||||
type Candidate = String;
|
||||
|
||||
fn complete(
|
||||
&self,
|
||||
line: &str,
|
||||
pos: usize,
|
||||
_ctx: &rustyline::Context<'_>,
|
||||
) -> rustyline::Result<(usize, Vec<Self::Candidate>)> {
|
||||
let line = &line[0..pos];
|
||||
if line == "" {
|
||||
Ok((0, vec![]))
|
||||
} else {
|
||||
let (start, token) = if line.starts_with(':') {
|
||||
(0, line)
|
||||
} else {
|
||||
// Same as editline's SEPS, except for double and single quotes:
|
||||
extract_word(line, pos, None, |c| {
|
||||
"#$&()*:;<=>?[\\]^`{,}~\n\t ".contains(c)
|
||||
})
|
||||
};
|
||||
|
||||
Ok((start, self.cxx.complete(token)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Rustyline<'a> {
|
||||
editor: Editor<Helper<'a>, FileHistory>,
|
||||
history_file: PathBuf,
|
||||
}
|
||||
|
||||
impl<'a> Rustyline<'a> {
|
||||
pub fn new(
|
||||
history_file: impl AsRef<Path>,
|
||||
completer: &'a CxxCompleter,
|
||||
) -> Result<Self, Box<dyn Error>> {
|
||||
let history_file = history_file.as_ref();
|
||||
|
||||
// TODO add settings and stuff
|
||||
let config = Config::builder()
|
||||
.auto_add_history(true)
|
||||
.max_history_size(1000)?
|
||||
.history_ignore_dups(true)?
|
||||
.completion_type(CompletionType::List)
|
||||
.build();
|
||||
let history = FileHistory::with_config(&config);
|
||||
let mut editor = Editor::with_history(config, history)?;
|
||||
editor.set_helper(Some(Helper { cxx: completer }));
|
||||
// ignore history load errors for historical reasons
|
||||
// TODO stop that some day
|
||||
let _ = editor.load_history(history_file);
|
||||
Ok(Rustyline {
|
||||
editor,
|
||||
history_file: history_file.to_path_buf(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn ask(&mut self, prompt: &str) -> Result<String, ReadlineError> {
|
||||
self.editor.readline(prompt)
|
||||
}
|
||||
|
||||
pub fn write_history(&mut self) {
|
||||
if let Err(e) = self.editor.save_history(&self.history_file) {
|
||||
log_warning!("failed to write history file: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
mod crate::repl {
|
||||
use ::std::string::String as String;
|
||||
use ::std::vec::Vec as Vec;
|
||||
use ::std::error::Error as Error;
|
||||
use ::std::result::Result as Result;
|
||||
use ::rustyline::error::ReadlineError as ReadlineError;
|
||||
|
||||
#cpp_additional_includes "
|
||||
namespace nix::detail {
|
||||
struct ReplCompleterMixin;
|
||||
}
|
||||
"
|
||||
|
||||
type CxxCompleter {
|
||||
#cpp_ref "::nix::detail::ReplCompleterMixin";
|
||||
}
|
||||
|
||||
extern "C++" {
|
||||
impl ReplCompleter for CxxCompleter {
|
||||
safe fn complete(&self, &str) -> Vec<String>;
|
||||
}
|
||||
}
|
||||
|
||||
type ReadlineError {
|
||||
#layout_conservative(size = 16, align = 8);
|
||||
|
||||
constructor Eof;
|
||||
constructor Interrupted;
|
||||
constructor Io(::std::io::Error);
|
||||
|
||||
fn into(self) -> Box<dyn Error>;
|
||||
}
|
||||
|
||||
type Rustyline {
|
||||
#heap_allocated;
|
||||
|
||||
fn new(&str, &CxxCompleter) -> Result<Rustyline, Box<dyn Error>>;
|
||||
fn ask(&mut self, &str) -> Result<String, ReadlineError>;
|
||||
fn write_history(&mut self);
|
||||
}
|
||||
|
||||
type Result<Rustyline, Box<dyn Error>> {
|
||||
#heap_allocated;
|
||||
|
||||
constructor Ok(Rustyline);
|
||||
constructor Err(Box<dyn Error>);
|
||||
|
||||
fn unwrap(self) -> Rustyline;
|
||||
fn unwrap_err(self) -> Box<dyn Error>;
|
||||
}
|
||||
|
||||
type Result<String, ReadlineError> {
|
||||
#layout_conservative(size = 24, align = 8);
|
||||
|
||||
constructor Ok(String);
|
||||
constructor Err(ReadlineError);
|
||||
|
||||
fn unwrap(self) -> String;
|
||||
fn unwrap_err(self) -> ReadlineError;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -7,6 +7,6 @@ Description: Lix Package Manager (libraries)
|
||||
Version: @PACKAGE_VERSION@
|
||||
# dependencies on boost, libarchive, nlohmann_json are omitted since they are optional (only required by some headers)
|
||||
Requires: lix-base kj-async @BOEHM_IF_FOUND@
|
||||
Requires.private: @AWS_SDK_IF_FOUND@ @BOEHM_IF_FOUND@ @CPUID_IF_FOUND@ @SECCOMP_IF_FOUND@ libeditline lowdown ncurses libbrotlidec libbrotlienc libarchive libcrypto capnp-rpc libcurl sqlite3
|
||||
Requires.private: @AWS_SDK_IF_FOUND@ @BOEHM_IF_FOUND@ @CPUID_IF_FOUND@ @SECCOMP_IF_FOUND@ lowdown libbrotlidec libbrotlienc libarchive libcrypto capnp-rpc libcurl sqlite3
|
||||
Cflags: -I${includedir}
|
||||
Libs: -L${libdir} -llix
|
||||
|
||||
@@ -139,12 +139,10 @@ dependencies = [
|
||||
capnp_rpc,
|
||||
cpuid,
|
||||
curl,
|
||||
editline,
|
||||
kj,
|
||||
libarchive,
|
||||
libatomic,
|
||||
lowdown,
|
||||
ncurses,
|
||||
nlohmann_json,
|
||||
openssl,
|
||||
seccomp,
|
||||
|
||||
@@ -379,7 +379,6 @@ brotli = [
|
||||
]
|
||||
|
||||
openssl = dependency('libcrypto', 'openssl', required : true, include_type : 'system')
|
||||
ncurses = dependency('ncurses', required : true, include_type : 'system')
|
||||
|
||||
# FIXME: confirm we actually support such old versions of aws-sdk-cpp
|
||||
aws_sdk = dependency('aws-cpp-sdk-core', required : false, version : '>=1.8', include_type : 'system')
|
||||
@@ -432,8 +431,6 @@ sqlite = dependency('sqlite3', 'sqlite', version : '>=3.6.19', required : true,
|
||||
|
||||
curl = dependency('libcurl', 'curl', required : true, include_type : 'system')
|
||||
|
||||
editline = dependency('libeditline', 'editline', version : '>=1.14', required : true, include_type : 'system')
|
||||
|
||||
lowdown = dependency('lowdown', version : '>=1.4.0', required : true, include_type : 'system')
|
||||
|
||||
# TODO(sterni): drop the corresponding #ifdef after NixOS 25.11 is EOL which still distributes lowdown < 3.0.0
|
||||
|
||||
-18
@@ -24,8 +24,6 @@
|
||||
curl-lix ? __forDefaults.curl-lix,
|
||||
curl,
|
||||
doxygen,
|
||||
editline-lix ? __forDefaults.editline-lix,
|
||||
editline,
|
||||
git,
|
||||
gtest,
|
||||
jq,
|
||||
@@ -45,7 +43,6 @@
|
||||
mercurial,
|
||||
meson,
|
||||
ninja,
|
||||
ncurses,
|
||||
openssl,
|
||||
passt,
|
||||
pegtl,
|
||||
@@ -132,19 +129,6 @@
|
||||
);
|
||||
});
|
||||
|
||||
editline-lix = editline.overrideAttrs (prev: {
|
||||
configureFlags = (prev.configureFlags or [ ]) ++ [
|
||||
# Enable SIGSTOP (Ctrl-Z) behavior.
|
||||
(lib.enableFeature true "sigstop")
|
||||
# Enable ANSI arrow keys.
|
||||
(lib.enableFeature true "arrow-keys")
|
||||
# Use termcap library to query terminal size.
|
||||
(lib.enableFeature true "termcap")
|
||||
];
|
||||
|
||||
propagatedBuildInputs = (prev.propagatedBuildInputs or [ ]) ++ [ ncurses ];
|
||||
});
|
||||
|
||||
# Avoid a bunch of build closure of the tracer, we just need the dtrace
|
||||
# generator.
|
||||
systemtap-lix = buildPackages.linuxPackages.systemtap.override { withStap = false; };
|
||||
@@ -548,7 +532,6 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
bzip2
|
||||
xz
|
||||
brotli
|
||||
editline-lix
|
||||
openssl
|
||||
sqlite
|
||||
libarchive
|
||||
@@ -757,7 +740,6 @@ stdenv.mkDerivation (finalAttrs: {
|
||||
inherit (__forDefaults)
|
||||
boehmgc-nix
|
||||
curl-lix
|
||||
editline-lix
|
||||
build-release-notes
|
||||
pegtl
|
||||
;
|
||||
|
||||
@@ -4,7 +4,6 @@ libstoreconsumer_tester = executable(
|
||||
dependencies : [
|
||||
libasanoptions,
|
||||
liblix,
|
||||
editline,
|
||||
boost,
|
||||
lowdown,
|
||||
kj,
|
||||
|
||||
Reference in New Issue
Block a user