diff --git a/Cargo.lock b/Cargo.lock
index cb0b380d0..6b4e59c78 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -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"
diff --git a/Cargo.toml b/Cargo.toml
index 920ebfcbb..7c59d3c4c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,6 +11,8 @@ edition = "2021"
[workspace.dependencies]
clap = "4"
+rustyline = "18"
+rustyline-derive = "0.12"
syn = "2.0"
zngur = "0.10"
diff --git a/doc/manual/rl-next/rustyline.md b/doc/manual/rl-next/rustyline.md
new file mode 100644
index 000000000..cd7934712
--- /dev/null
+++ b/doc/manual/rl-next/rustyline.md
@@ -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.
diff --git a/doc/manual/src/installation/prerequisites-source.md b/doc/manual/src/installation/prerequisites-source.md
index 0ec2ec8c5..f711d854d 100644
--- a/doc/manual/src/installation/prerequisites-source.md
+++ b/doc/manual/src/installation/prerequisites-source.md
@@ -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 .
- - The `editline` library of version 1.14.0 or higher. It can be
- obtained from the its repository
- .
-
- 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
diff --git a/lix/libcmd/repl-interacter.cc b/lix/libcmd/repl-interacter.cc
index 04f4fd886..9f11864a3 100644
--- a/lix/libcmd/repl-interacter.cc
+++ b/lix/libcmd/repl-interacter.cc
@@ -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
-#include
+#include "lix/lix-rs/main.gen.hh"
+#include "lix/lix-rs/utils.hh"
#include
+#include
#include
-#include
-#include
-// 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
-}
-
-#include "lix/libutil/finally.hh"
-#include "lix/libutil/strings.hh"
#include "lix/libcmd/repl-interacter.hh"
+namespace rust {
+Vec Impl::complete(
+ Ref self, Ref input
+)
+try {
+ auto s = to_std_string(input);
+
+ auto result = std::vec::Vec::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::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(vp));
- }
- throw Error("allocation failure");
- }
- return p;
- };
- vp = check(static_cast(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(rl_point)};
-
- auto tokens = tokenizeString>(
- 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(std::move(ok)); },
+ [&](rust::Box> err) {
+ throw Error("%s", Uncolored(to_std_string(err.to_string())));
+ }
+ );
}
-static constexpr const char * promptForType(ReplPromptType promptType)
+static rust::Ref 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";
diff --git a/lix/libcmd/repl-interacter.hh b/lix/libcmd/repl-interacter.hh
index 40680724b..0fcdbabd2 100644
--- a/lix/libcmd/repl-interacter.hh
+++ b/lix/libcmd/repl-interacter.hh
@@ -1,11 +1,14 @@
#pragma once
/// @file
-#include "lix/libutil/finally.hh"
#include "lix/libutil/types.hh"
-#include
+#include
#include
+namespace rust::lix::repl {
+struct Rustyline;
+}
+
namespace nix {
namespace detail {
@@ -24,9 +27,7 @@ enum class ReplPromptType {
class ReplInteracter
{
public:
- using Guard = Finally>;
-
- 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 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;
};
diff --git a/lix/libcmd/repl.cc b/lix/libcmd/repl.cc
index 6af2c16fb..7caa28949 100644
--- a/lix/libcmd/repl.cc
+++ b/lix/libcmd/repl.cc
@@ -1,6 +1,5 @@
#include
#include
-#include
#include
#include
#include
@@ -380,7 +379,7 @@ ReplExitStatus NixRepl::mainLoop()
std::list loadables = std::exchange(loaded, {});
loadFiles(loadables);
- auto _guard = interacter->init(static_cast(this));
+ interacter->init(static_cast(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, `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);
}
}
diff --git a/lix/libutil/experimental-features/repl-automation.md b/lix/libutil/experimental-features/repl-automation.md
index c21cd4478..263e5b3c1 100644
--- a/lix/libutil/experimental-features/repl-automation.md
+++ b/lix/libutil/experimental-features/repl-automation.md
@@ -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.
diff --git a/lix/lix-rs/Cargo.toml b/lix/lix-rs/Cargo.toml
index 3d9daa121..442517622 100644
--- a/lix/lix-rs/Cargo.toml
+++ b/lix/lix-rs/Cargo.toml
@@ -12,6 +12,8 @@ default = []
[dependencies]
lix-doc.path = "../lix-doc"
+rustyline.workspace = true
+rustyline-derive.workspace = true
[build-dependencies]
zngur.workspace = true
diff --git a/lix/lix-rs/meson.build b/lix/lix-rs/meson.build
index b234546ed..4f89ec963 100644
--- a/lix/lix-rs/meson.build
+++ b/lix/lix-rs/meson.build
@@ -3,6 +3,7 @@ lix_rs_files += files(
# keep-sorted start
'src/ffi_test.rs',
'src/lib.rs',
+ 'src/repl.rs',
# keep-sorted end
)
diff --git a/lix/lix-rs/src/lib.rs b/lix/lix-rs/src/lib.rs
index f45ba2b79..44e5d19b2 100644
--- a/lix/lix-rs/src/lib.rs
+++ b/lix/lix-rs/src/lib.rs
@@ -146,3 +146,4 @@ pub(crate) mod log {
}
mod ffi_test;
+mod repl;
diff --git a/lix/lix-rs/src/lib.zng b/lix/lix-rs/src/lib.zng
index 3e163f92b..ac691b626 100644
--- a/lix/lix-rs/src/lib.zng
+++ b/lix/lix-rs/src/lib.zng
@@ -1,4 +1,5 @@
merge "./ffi_test.zng";
+merge "./repl.zng";
mod crate::ffi {
use ::std::result::Result as Result;
diff --git a/lix/lix-rs/src/repl.rs b/lix/lix-rs/src/repl.rs
new file mode 100644
index 000000000..014170385
--- /dev/null
+++ b/lix/lix-rs/src/repl.rs
@@ -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;
+}
+
+#[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)> {
+ 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, FileHistory>,
+ history_file: PathBuf,
+}
+
+impl<'a> Rustyline<'a> {
+ pub fn new(
+ history_file: impl AsRef,
+ completer: &'a CxxCompleter,
+ ) -> Result> {
+ 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 {
+ 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);
+ }
+ }
+}
diff --git a/lix/lix-rs/src/repl.zng b/lix/lix-rs/src/repl.zng
new file mode 100644
index 000000000..915e7090e
--- /dev/null
+++ b/lix/lix-rs/src/repl.zng
@@ -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;
+ }
+ }
+
+ type ReadlineError {
+ #layout_conservative(size = 16, align = 8);
+
+ constructor Eof;
+ constructor Interrupted;
+ constructor Io(::std::io::Error);
+
+ fn into(self) -> Box;
+ }
+
+ type Rustyline {
+ #heap_allocated;
+
+ fn new(&str, &CxxCompleter) -> Result>;
+ fn ask(&mut self, &str) -> Result;
+ fn write_history(&mut self);
+ }
+
+ type Result> {
+ #heap_allocated;
+
+ constructor Ok(Rustyline);
+ constructor Err(Box);
+
+ fn unwrap(self) -> Rustyline;
+ fn unwrap_err(self) -> Box;
+ }
+
+ type Result {
+ #layout_conservative(size = 24, align = 8);
+
+ constructor Ok(String);
+ constructor Err(ReadlineError);
+
+ fn unwrap(self) -> String;
+ fn unwrap_err(self) -> ReadlineError;
+ }
+}
diff --git a/lix/lix.pc.in b/lix/lix.pc.in
index 061d8805a..13c6bcdcb 100644
--- a/lix/lix.pc.in
+++ b/lix/lix.pc.in
@@ -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
diff --git a/lix/meson.build b/lix/meson.build
index 99e909845..45689caba 100644
--- a/lix/meson.build
+++ b/lix/meson.build
@@ -139,12 +139,10 @@ dependencies = [
capnp_rpc,
cpuid,
curl,
- editline,
kj,
libarchive,
libatomic,
lowdown,
- ncurses,
nlohmann_json,
openssl,
seccomp,
diff --git a/meson.build b/meson.build
index 18302a651..23e01e700 100644
--- a/meson.build
+++ b/meson.build
@@ -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
diff --git a/package.nix b/package.nix
index 5b0f08e6b..bd9433bb6 100644
--- a/package.nix
+++ b/package.nix
@@ -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
;
diff --git a/tests/functional/test-libstoreconsumer/meson.build b/tests/functional/test-libstoreconsumer/meson.build
index dd23077e6..26fea487d 100644
--- a/tests/functional/test-libstoreconsumer/meson.build
+++ b/tests/functional/test-libstoreconsumer/meson.build
@@ -4,7 +4,6 @@ libstoreconsumer_tester = executable(
dependencies : [
libasanoptions,
liblix,
- editline,
boost,
lowdown,
kj,