forked from lix-project/lix
Merge "repl: tab-complete quoted attribute names" into main
This commit is contained in:
commit
06996718c3
4 changed files with 129 additions and 50 deletions
|
@ -70,6 +70,10 @@ horrors:
|
||||||
iFreilicht:
|
iFreilicht:
|
||||||
github: iFreilicht
|
github: iFreilicht
|
||||||
|
|
||||||
|
ian-h-chamberlain:
|
||||||
|
forgejo: ian-h-chamberlain
|
||||||
|
github: ian-h-chamberlain
|
||||||
|
|
||||||
isabelroses:
|
isabelroses:
|
||||||
forgejo: isabelroses
|
forgejo: isabelroses
|
||||||
github: isabelroses
|
github: isabelroses
|
||||||
|
|
10
doc/manual/rl-next/repl-complete-quoted-attrs.md
Normal file
10
doc/manual/rl-next/repl-complete-quoted-attrs.md
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
---
|
||||||
|
synopsis: "`nix repl` correctly tab-completes attribute names that require quotes"
|
||||||
|
cls: [1783]
|
||||||
|
credits: [ian-h-chamberlain]
|
||||||
|
category: Improvements
|
||||||
|
---
|
||||||
|
|
||||||
|
The REPL (`nix repl`) now includes quotes as part of attribute names while completing with `<TAB>`,
|
||||||
|
if necessary. For example, attribute names like `"hello@example.com"` or `"hello world"` would
|
||||||
|
be suggested without quotes, resulting in invalid syntax.
|
|
@ -18,6 +18,7 @@ extern "C" {
|
||||||
}
|
}
|
||||||
|
|
||||||
#include "lix/libutil/finally.hh"
|
#include "lix/libutil/finally.hh"
|
||||||
|
#include "lix/libutil/strings.hh"
|
||||||
#include "lix/libcmd/repl-interacter.hh"
|
#include "lix/libcmd/repl-interacter.hh"
|
||||||
|
|
||||||
namespace nix {
|
namespace nix {
|
||||||
|
@ -30,52 +31,14 @@ void sigintHandler(int signo)
|
||||||
{
|
{
|
||||||
g_signal_received = signo;
|
g_signal_received = signo;
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
static detail::ReplCompleterMixin * curRepl; // ugly
|
static detail::ReplCompleterMixin * curRepl; // ugly
|
||||||
|
|
||||||
static char * completionCallback(char * s, int * match)
|
/**
|
||||||
|
* @return a null-terminated list of completions as expected by `el_print_columns`
|
||||||
|
*/
|
||||||
|
char ** copyCompletions(const StringSet& possible)
|
||||||
{
|
{
|
||||||
auto possible = curRepl->completePrefix(s);
|
|
||||||
if (possible.size() == 1) {
|
|
||||||
*match = 1;
|
|
||||||
auto * res = strdup(possible.begin()->c_str() + strlen(s));
|
|
||||||
if (!res)
|
|
||||||
throw Error("allocation failure");
|
|
||||||
return res;
|
|
||||||
} else if (possible.size() > 1) {
|
|
||||||
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 = strlen(s);
|
|
||||||
size_t len = 0;
|
|
||||||
while (checkAllHaveSameAt(start + len))
|
|
||||||
++len;
|
|
||||||
if (len > 0) {
|
|
||||||
*match = 1;
|
|
||||||
auto * res = strdup(std::string(*possible.begin(), start, len).c_str());
|
|
||||||
if (!res)
|
|
||||||
throw Error("allocation failure");
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
*match = 0;
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int listPossibleCallback(char * s, char *** avp)
|
|
||||||
{
|
|
||||||
auto possible = curRepl->completePrefix(s);
|
|
||||||
|
|
||||||
if (possible.size() > (INT_MAX / sizeof(char *)))
|
|
||||||
throw Error("too many completions");
|
|
||||||
|
|
||||||
int ac = 0;
|
int ac = 0;
|
||||||
char ** vp = nullptr;
|
char ** vp = nullptr;
|
||||||
|
|
||||||
|
@ -90,15 +53,95 @@ static int listPossibleCallback(char * s, char *** avp)
|
||||||
}
|
}
|
||||||
return p;
|
return p;
|
||||||
};
|
};
|
||||||
|
|
||||||
vp = check(static_cast<char **>(malloc(possible.size() * sizeof(char *))));
|
vp = check(static_cast<char **>(malloc(possible.size() * sizeof(char *))));
|
||||||
|
|
||||||
for (auto & p : possible)
|
for (auto & p : possible)
|
||||||
vp[ac++] = check(strdup(p.c_str()));
|
vp[ac++] = check(strdup(p.c_str()));
|
||||||
|
|
||||||
*avp = vp;
|
return vp;
|
||||||
|
}
|
||||||
|
|
||||||
return ac;
|
// 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(completion.c_str() + 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(commonPrefix.c_str());
|
||||||
|
el_ring_bell();
|
||||||
|
return redisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
char** columns = copyCompletions(possible);
|
||||||
|
el_print_columns(possible.size(), columns);
|
||||||
|
return redisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
ReadlineLikeInteracter::Guard ReadlineLikeInteracter::init(detail::ReplCompleterMixin * repl)
|
ReadlineLikeInteracter::Guard ReadlineLikeInteracter::init(detail::ReplCompleterMixin * repl)
|
||||||
|
@ -115,8 +158,10 @@ ReadlineLikeInteracter::Guard ReadlineLikeInteracter::init(detail::ReplCompleter
|
||||||
auto oldRepl = curRepl;
|
auto oldRepl = curRepl;
|
||||||
curRepl = repl;
|
curRepl = repl;
|
||||||
Guard restoreRepl([oldRepl] { curRepl = oldRepl; });
|
Guard restoreRepl([oldRepl] { curRepl = oldRepl; });
|
||||||
rl_set_complete_func(completionCallback);
|
// editline does its own escaping of completions, so we rebind tab
|
||||||
rl_set_list_possib_func(listPossibleCallback);
|
// to our own completion function to skip that and do nix escaping
|
||||||
|
// instead of shell escaping.
|
||||||
|
el_bind_key(CTL('I'), doCompletion);
|
||||||
return restoreRepl;
|
return restoreRepl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
|
|
||||||
#include "lix/libutil/ansicolor.hh"
|
#include "lix/libutil/ansicolor.hh"
|
||||||
#include "lix/libmain/shared.hh"
|
#include "lix/libmain/shared.hh"
|
||||||
|
#include "lix/libutil/escape-string.hh"
|
||||||
#include "lix/libexpr/eval.hh"
|
#include "lix/libexpr/eval.hh"
|
||||||
#include "lix/libexpr/eval-cache.hh"
|
#include "lix/libexpr/eval-cache.hh"
|
||||||
#include "lix/libexpr/eval-inline.hh"
|
#include "lix/libexpr/eval-inline.hh"
|
||||||
|
@ -415,6 +416,22 @@ StringSet NixRepl::completePrefix(const std::string & prefix)
|
||||||
i++;
|
i++;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// To handle cases like `foo."bar.`, walk back the cursor
|
||||||
|
// to the previous dot if there are an odd number of quotes.
|
||||||
|
auto quoteCount =
|
||||||
|
std::count_if(cur.begin(), cur.begin() + dot, [](char c) { return c == '"'; });
|
||||||
|
if (quoteCount % 2 != 0) {
|
||||||
|
// Find the last quote before the dot
|
||||||
|
auto prevQuote = cur.rfind('"', dot - 1);
|
||||||
|
if (prevQuote != std::string::npos) {
|
||||||
|
// And the previous dot prior to that quote
|
||||||
|
auto prevDot = cur.rfind('.', prevQuote);
|
||||||
|
if (prevDot != std::string::npos) {
|
||||||
|
dot = prevDot;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Temporarily disable the debugger, to avoid re-entering readline. */
|
/* Temporarily disable the debugger, to avoid re-entering readline. */
|
||||||
auto debug = std::move(evaluator.debug);
|
auto debug = std::move(evaluator.debug);
|
||||||
Finally restoreDebug([&]() { evaluator.debug = std::move(debug); });
|
Finally restoreDebug([&]() { evaluator.debug = std::move(debug); });
|
||||||
|
@ -431,7 +448,10 @@ StringSet NixRepl::completePrefix(const std::string & prefix)
|
||||||
state.forceAttrs(v, noPos, "while evaluating an attrset for the purpose of completion (this error should not be displayed; file an issue?)");
|
state.forceAttrs(v, noPos, "while evaluating an attrset for the purpose of completion (this error should not be displayed; file an issue?)");
|
||||||
|
|
||||||
for (auto & i : *v.attrs) {
|
for (auto & i : *v.attrs) {
|
||||||
std::string_view name = evaluator.symbols[i.name];
|
std::ostringstream output;
|
||||||
|
printAttributeName(output, evaluator.symbols[i.name]);
|
||||||
|
std::string name = output.str();
|
||||||
|
|
||||||
if (name.substr(0, cur2.size()) != cur2) continue;
|
if (name.substr(0, cur2.size()) != cur2) continue;
|
||||||
completions.insert(concatStrings(prev, expr, ".", name));
|
completions.insert(concatStrings(prev, expr, ".", name));
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue