libexpr: hyperlink attr names to their definition locations
Concept: what if you could, in your fancy terminal, in the year of our lord 2025, just click on the attrs you're looking at to go to where they're defined. Currently we only expose this info as builtins.unsafeGetAttrPos, which is inconvenient as it's not discoverable to users. By putting it in this more visible yet invisible spot, it's more likely to be more useful to more people. In the current state, this is not the most useful ever due to stuff like https://github.com/neovim/neovim/discussions/35097. However, it can be expanded by perhaps adding something like the url format setting ripgrep has. Change-Id: I3947f97d5c2056d59099af468d7b855486438227
This commit is contained in:
@@ -0,0 +1,16 @@
|
||||
---
|
||||
synopsis: Add hyperlinks in attr set printing
|
||||
issues: []
|
||||
cls: [3790]
|
||||
category: Features
|
||||
credits: [jade]
|
||||
---
|
||||
|
||||
The attribute set printer, such as is seen in `nix repl` or in type errors, now prints hyperlinks on each attribute name to its definition site if it is known.
|
||||
|
||||
Example: all of the attributes shown here are hyperlinks to the exact definition site of the attribute in question:
|
||||
|
||||
```
|
||||
$ nix eval -f '<nixpkgs>' lib.licenses.mit
|
||||
{ deprecated = false; free = true; fullName = "MIT License"; redistributable = true; shortName = "mit"; spdxId = "MIT"; url = "https://spdx.org/licenses/MIT.html"; }
|
||||
```
|
||||
@@ -159,6 +159,7 @@ public:
|
||||
|
||||
Value & alloc(std::string_view name, PosIdx pos = noPos);
|
||||
|
||||
[[nodiscard("must use created bindings")]]
|
||||
Bindings * finish()
|
||||
{
|
||||
bindings->sort();
|
||||
|
||||
+23
-8
@@ -2,6 +2,7 @@
|
||||
#include <span>
|
||||
#include <unordered_set>
|
||||
#include <sstream>
|
||||
#include <variant>
|
||||
|
||||
#include "lix/libutil/escape-string.hh"
|
||||
#include "lix/libexpr/print.hh"
|
||||
@@ -10,6 +11,7 @@
|
||||
#include "lix/libutil/signals.hh"
|
||||
#include "lix/libexpr/eval.hh"
|
||||
#include "lix/libutil/print-elided.hh"
|
||||
#include "lix/libutil/source-path.hh"
|
||||
#include "lix/libutil/terminal.hh"
|
||||
|
||||
namespace nix {
|
||||
@@ -90,7 +92,7 @@ bool isImportantAttrName(const std::string& attrName)
|
||||
return attrName == "type" || attrName == "_type";
|
||||
}
|
||||
|
||||
typedef std::pair<std::string, Value *> AttrPair;
|
||||
typedef std::pair<std::string, Attr const *> AttrPair;
|
||||
|
||||
struct ImportantFirstAttrNameCmp
|
||||
{
|
||||
@@ -105,7 +107,7 @@ struct ImportantFirstAttrNameCmp
|
||||
};
|
||||
|
||||
typedef std::set<const void *> ValuesSeen;
|
||||
typedef std::vector<std::pair<std::string, Value *>> AttrVec;
|
||||
typedef std::vector<std::pair<std::string, Attr const *>> AttrVec;
|
||||
|
||||
class Printer
|
||||
{
|
||||
@@ -259,18 +261,18 @@ private:
|
||||
}
|
||||
|
||||
auto item = v[0].second;
|
||||
if (!item) {
|
||||
if (!item->value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (options.force) {
|
||||
// The item is going to be forced during printing anyway, but we need its type now.
|
||||
state.forceValue(*item, noPos);
|
||||
state.forceValue(*item->value, noPos);
|
||||
}
|
||||
|
||||
// Pretty-print single-item attrsets only if they contain nested
|
||||
// structures.
|
||||
auto itemType = item->type();
|
||||
auto itemType = item->value->type();
|
||||
return itemType == nList || itemType == nAttrs;
|
||||
}
|
||||
|
||||
@@ -286,7 +288,7 @@ private:
|
||||
|
||||
AttrVec sorted;
|
||||
for (auto & i : *v.attrs)
|
||||
sorted.emplace_back(state.ctx.symbols[i.name], i.value);
|
||||
sorted.emplace_back(state.ctx.symbols[i.name], &i);
|
||||
|
||||
if (options.maxAttrs == std::numeric_limits<size_t>::max())
|
||||
std::sort(sorted.begin(), sorted.end());
|
||||
@@ -304,9 +306,22 @@ private:
|
||||
break;
|
||||
}
|
||||
|
||||
printAttributeName(output, i.first);
|
||||
std::ostringstream name;
|
||||
printAttributeName(name, i.first);
|
||||
|
||||
auto pos = state.ctx.positions[i.second->pos];
|
||||
if (auto path = std::get_if<CheckedSourcePath>(&pos.origin);
|
||||
path && options.ansiColors)
|
||||
{
|
||||
output << makeHyperlink(
|
||||
name.str(), makeHyperlinkLocalPath(path->to_string(), pos.line)
|
||||
);
|
||||
} else {
|
||||
output << name.str();
|
||||
}
|
||||
|
||||
output << " = ";
|
||||
print(*i.second, depth + 1);
|
||||
print(*i.second->value, depth + 1);
|
||||
output << ";";
|
||||
attrsPrinted++;
|
||||
printedHere++;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
#include "lix/libutil/terminal.hh"
|
||||
#include "fmt.hh"
|
||||
#include "lix/libutil/environment-variables.hh"
|
||||
#include "lix/libutil/sync.hh"
|
||||
#include "url.hh"
|
||||
|
||||
#include <sys/ioctl.h>
|
||||
#include <unistd.h>
|
||||
#include <limits.h>
|
||||
|
||||
namespace nix {
|
||||
|
||||
@@ -198,4 +201,55 @@ std::pair<unsigned short, unsigned short> getWindowSize()
|
||||
return *windowSize.lock();
|
||||
}
|
||||
|
||||
std::string makeHyperlink(std::string_view linkText, std::string_view target)
|
||||
{
|
||||
// 700 is arbitrarily chosen as a length limit as it's where screen breaks
|
||||
// according to https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda#length-limits
|
||||
if (target.empty() || target.length() > 700) {
|
||||
return std::string{linkText};
|
||||
}
|
||||
|
||||
#define OSC "\e]"
|
||||
#define ST "\e\\"
|
||||
|
||||
return fmt(OSC "8;;%s" ST "%s" OSC "8;;" ST, target, linkText);
|
||||
|
||||
#undef OSC
|
||||
#undef ST
|
||||
}
|
||||
|
||||
std::string makeHyperlinkLocalPath(std::string_view path, std::optional<unsigned> lineNumber)
|
||||
{
|
||||
// File paths in OSC 8 are required to have the hostname in them per the
|
||||
// spec.
|
||||
static std::string theHostname = []() -> std::string {
|
||||
// According to POSIX if the hostname is too long, there is no guarantee of
|
||||
// null termination so let's make sure there's always one.
|
||||
char theHostname_[_POSIX_HOST_NAME_MAX + 1] = {};
|
||||
|
||||
int err = gethostname(theHostname_, sizeof(theHostname_) - 1);
|
||||
// Who knows why getting the hostname would fail, but it is fallible!
|
||||
if (err < 0) {
|
||||
return "localhost";
|
||||
} else {
|
||||
return theHostname_;
|
||||
}
|
||||
}();
|
||||
|
||||
if (!path.starts_with('/')) {
|
||||
// Problematic to have non absolute paths
|
||||
return "";
|
||||
}
|
||||
auto content = percentEncode(path, "/");
|
||||
|
||||
// XXX(jade): these schemes are not standardized and even the file link
|
||||
// line number has no guarantee to work (and in fact theoretically is
|
||||
// supported in kitty but in practice is mostly ignored).
|
||||
// https://github.com/BurntSushi/ripgrep/blob/bf63fe8f258afc09bae6caa48f0ae35eaf115005/crates/printer/src/hyperlink_aliases.rs#L4-L22
|
||||
auto result = fmt("file://%s%s", theHostname, content);
|
||||
if (lineNumber.has_value()) {
|
||||
result += fmt("#%d", *lineNumber);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
///@file
|
||||
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace nix {
|
||||
@@ -69,4 +70,22 @@ void updateWindowSize();
|
||||
*/
|
||||
std::pair<unsigned short, unsigned short> getWindowSize();
|
||||
|
||||
/**
|
||||
* Makes a terminal hyperlink using OSC 8.
|
||||
*
|
||||
* If the link target is too long (700 bytes is the current limit), the link is
|
||||
* skipped and the link text is emitted as-is. This limits the maximum amount
|
||||
* of context required to a manageable amount that doesn't break any terminals.
|
||||
*
|
||||
* See: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
|
||||
*
|
||||
* @see makeHyperlinkLocalPath
|
||||
*/
|
||||
std::string makeHyperlink(std::string_view linkText, std::string_view target);
|
||||
|
||||
/**
|
||||
* Creates an OSC 8 compliant `file://` path for a given filesystem path.
|
||||
*/
|
||||
std::string
|
||||
makeHyperlinkLocalPath(std::string_view path, std::optional<unsigned> lineNumber = std::nullopt);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
#include "lix/libutil/canon-path.hh"
|
||||
#include "lix/libutil/source-path.hh"
|
||||
#include "lix/libutil/terminal.hh"
|
||||
#include "tests/libexpr.hh"
|
||||
|
||||
#include "lix/libexpr/value.hh"
|
||||
@@ -774,4 +777,25 @@ TEST_F(ValuePrintingTests, ansiColorsListElided)
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(ValuePrintingTests, osc8InAttrSets)
|
||||
{
|
||||
const auto arbitrarySource = SourcePath(CanonPath("/dev/null")).unsafeIntoChecked();
|
||||
auto origin = evaluator.positions.addOrigin(Pos::Origin(arbitrarySource), 0);
|
||||
auto pos = evaluator.positions.add(origin, 0);
|
||||
BindingsBuilder builder = evaluator.buildBindings(1);
|
||||
|
||||
auto vZero = Value{NewValueAs::integer, NixInt{0}};
|
||||
|
||||
builder.insert(evaluator.symbols.create("x"), &vZero, pos);
|
||||
auto vAttrs = Value{NewValueAs::attrs, builder.finish()};
|
||||
|
||||
auto hyperlink = makeHyperlink("x", makeHyperlinkLocalPath("/dev/null", 1));
|
||||
|
||||
test(
|
||||
vAttrs,
|
||||
"{ " + hyperlink + " = " ANSI_CYAN "0" ANSI_NORMAL "; }",
|
||||
PrintOptions{.ansiColors = true}
|
||||
);
|
||||
}
|
||||
|
||||
} // namespace nix
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "lix/libutil/terminal.hh"
|
||||
#include <gtest/gtest.h>
|
||||
#include <regex>
|
||||
|
||||
namespace nix {
|
||||
|
||||
@@ -176,4 +177,23 @@ TEST(filterANSIEscapes, controlChars) {
|
||||
EXPECT_EQ(filterANSIEscapes("foo\v\n\fbar", false, 8), "foo\v\n\fba");
|
||||
}
|
||||
|
||||
TEST(makeHyperlink, works)
|
||||
{
|
||||
auto big = std::string(701, 'A');
|
||||
EXPECT_EQ(makeHyperlink(big, "meow"), "\e]8;;meow\e\\" + big + "\e]8;;\e\\");
|
||||
EXPECT_EQ(makeHyperlink("meow", big), "meow");
|
||||
}
|
||||
|
||||
TEST(makeHyperlinkLocalPath, works)
|
||||
{
|
||||
// NOLINTNEXTLINE(lix-foreign-exceptions): its a test lol
|
||||
auto regex = std::regex{R""(^file://([^/]+)/(.*)$)""};
|
||||
std::smatch match;
|
||||
auto output = makeHyperlinkLocalPath("/a/b/ c", 4);
|
||||
|
||||
ASSERT_TRUE(std::regex_match(output, match, regex));
|
||||
// Hostname has a value
|
||||
ASSERT_GT(match[1].length(), 0);
|
||||
ASSERT_EQ(match[2].str(), "a/b/%20c#4");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user