Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nixd: add hover support #22

Merged
merged 12 commits into from
May 27, 2023
1 change: 1 addition & 0 deletions lib/nixd/include/nixd/CallbackExpr.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ using ExprCallback =
static Callback##EXPR *create(ASTContext &Cxt, const nix::EXPR &E, \
ExprCallback ECB); \
void eval(nix::EvalState &State, nix::Env &Env, nix::Value &V) override; \
std::string getName(); \
};
#include "NixASTNodes.inc"
#undef NIX_EXPR
Expand Down
10 changes: 10 additions & 0 deletions lib/nixd/include/nixd/Expr.h
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,14 @@ template <class Derived> struct RecursiveASTVisitor {
#undef DEF_TRAVERSE_TYPE
#undef TRY_TO_TRAVERSE
#undef TRY_TO

inline const char *getExprName(nix::Expr *E) {
#define NIX_EXPR(EXPR) \
if (dynamic_cast<const nix::EXPR *>(E)) { \
return #EXPR; \
}
#include "NixASTNodes.inc"
#undef NIX_EXPR
}

} // namespace nixd
10 changes: 9 additions & 1 deletion lib/nixd/include/nixd/Server.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
#pragma once

#include "nixd/EvalDraftStore.h"

#include "lspserver/Connection.h"
#include "lspserver/DraftStore.h"
#include "lspserver/Function.h"
Expand Down Expand Up @@ -33,7 +35,8 @@ bool fromJSON(const llvm::json::Value &Params, InstallableConfigurationItem &R,
/// The server instance, nix-related language features goes here
class Server : public lspserver::LSPServer {

lspserver::DraftStore DraftMgr;
EvalDraftStore DraftMgr;
boost::asio::thread_pool Pool;

lspserver::ClientCapabilities ClientCaps;

Expand Down Expand Up @@ -64,6 +67,7 @@ class Server : public lspserver::LSPServer {
std::unique_ptr<lspserver::OutboundPort> Out)
: LSPServer(std::move(In), std::move(Out)) {
Registry.addMethod("initialize", this, &Server::onInitialize);
Registry.addMethod("textDocument/hover", this, &Server::onHover);
Registry.addNotification("initialized", this, &Server::onInitialized);

// Text Document Synchronization
Expand All @@ -82,6 +86,8 @@ class Server : public lspserver::LSPServer {
"workspace/configuration");
}

~Server() override { Pool.join(); }

void fetchConfig();

void onInitialize(const lspserver::InitializeParams &,
Expand All @@ -104,6 +110,8 @@ class Server : public lspserver::LSPServer {
const lspserver::DidChangeConfigurationParams &) {
fetchConfig();
}
void onHover(const lspserver::TextDocumentPositionParams &,
lspserver::Callback<llvm::json::Value>);
};

}; // namespace nixd
4 changes: 2 additions & 2 deletions lib/nixd/src/EvalDraftStore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ void EvalDraftStore::withEvaluation(
Forest.insert({ActiveFile, nix::make_ref<EvalAST>(FileAST)});
Forest.at(ActiveFile)->preparePositionLookup(*State);
Forest.at(ActiveFile)->injectAST(*State, ActiveFile);
} catch (const nix::ParseError &) {
// Ignore parsing errors, because workspace file might be incomplete.
} catch (const nix::Error &) {
// Ignore caching errors, because workspace file might be incomplete.
}
}

Expand Down
84 changes: 82 additions & 2 deletions lib/nixd/src/Server.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#include "nixd/Server.h"
#include "nixd/Diagnostic.h"
#include "nixd/Expr.h"

#include "lspserver/Logger.h"
#include "lspserver/Path.h"
#include "lspserver/Protocol.h"

Expand All @@ -11,8 +13,16 @@

#include <nix/eval.hh>
#include <nix/store-api.hh>
#include <nix/util.hh>

#include <exception>
#include <filesystem>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <variant>

namespace fs = std::filesystem;
namespace nixd {

Expand Down Expand Up @@ -50,6 +60,7 @@ void Server::onInitialize(const lspserver::InitializeParams &InitializeParams,
{"change", (int)lspserver::TextDocumentSyncKind::Incremental},
{"save", true},
}},
{"hoverProvider", true},
};

llvm::json::Object Result{
Expand Down Expand Up @@ -128,8 +139,8 @@ void Server::publishStandaloneDiagnostic(lspserver::URIForFile Uri,
auto NixState = std::make_unique<nix::EvalState>(nix::Strings{}, NixStore);
try {
fs::path Path = Uri.file().str();
auto *E =
NixState->parseExprFromString(std::move(Content), Path.remove_filename());
auto *E = NixState->parseExprFromString(std::move(Content),
inclyc marked this conversation as resolved.
Show resolved Hide resolved
Path.remove_filename());
nix::Value V;
NixState->eval(E, V);
} catch (const nix::Error &PE) {
Expand All @@ -143,4 +154,73 @@ void Server::publishStandaloneDiagnostic(lspserver::URIForFile Uri,
.uri = Uri, .diagnostics = {}, .version = LSPVersion});
}

void Server::onHover(const lspserver::TextDocumentPositionParams &Paras,
lspserver::Callback<llvm::json::Value> Reply) {
std::string HoverFile = Paras.textDocument.uri.file().str();

// Helper lambda that retrieve installable cmdline
auto GetInstallableFromConfig = [&]() {
if (auto Installable = Config.installable) {
auto ConfigArgs = Installable->args;
lspserver::log("using client specified installable: [{0}] {1}",
llvm::iterator_range(ConfigArgs.begin(), ConfigArgs.end()),
Installable->installable);
return std::tuple{nix::Strings(ConfigArgs.begin(), ConfigArgs.end()),
Installable->installable};
}
// Fallback to current file otherwise.
return std::tuple{nix::Strings{"--file", HoverFile}, std::string{""}};
};

auto [CommandLine, Installable] = GetInstallableFromConfig();

DraftMgr.withEvaluation(
Pool, CommandLine, Installable,
[=, Reply = std::move(Reply)](
std::variant<std::exception *,
nix::ref<EvalDraftStore::EvaluationResult>>
EvalResult) mutable {
auto ActionOnResult =
[&](const nix::ref<EvalDraftStore::EvaluationResult> &Result) {
auto Forest = Result->EvalASTForest;
try {
auto AST = Forest.at(HoverFile);
auto *Node = AST->lookupPosition(Paras.position);
const auto *ExprName = getExprName(Node);
std::string HoverText;
try {
auto Value = AST->getValue(Node);
std::stringstream Res{};
Value.print(Result->State->symbols, Res);
HoverText = llvm::formatv("## {0} \n Value: `{1}`", ExprName,
Res.str());
} catch (const std::out_of_range &) {
// No such value, just reply dummy item
std::stringstream NodeOut;
Node->show(Result->State->symbols, NodeOut);
lspserver::vlog("no associated value on node {0}!",
NodeOut.str());
HoverText = llvm::formatv("`{0}`", ExprName);
}
Reply(lspserver::Hover{{
lspserver::MarkupKind::Markdown,
HoverText,
},
std::nullopt});
} catch (const std::out_of_range &) {
// Probably out of range in Forest.at
// Ignore this expression, and reply dummy value.
Reply(lspserver::Hover{{}, std::nullopt});
}
};
auto ActionOnExcept = [&](std::exception *Except) {
// TODO: publish evaluation diagnostic
lspserver::log("evaluation error: {0}", Except->what());
Reply(lspserver::Hover{{}, std::nullopt});
};

std::visit(nix::overloaded{ActionOnResult, ActionOnExcept}, EvalResult);
});
}

} // namespace nixd
101 changes: 101 additions & 0 deletions tools/nixd/test/hover-eval-error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# RUN: nixd --lit-test < %s | FileCheck %s

<-- initialize(0)

```json
{
"jsonrpc":"2.0",
"id":0,
"method":"initialize",
"params":{
"processId":123,
"rootPath":"",
"capabilities":{
},
"trace":"off"
}
}
```


<-- textDocument/didOpen

File #1:

```nix
let
pkgs = {
a = 1;
};
in
with pkgs;

a
```

```json
{
"jsonrpc":"2.0",
"method":"textDocument/didOpen",
"params":{
"textDocument":{
"uri":"file:///with.nix",
"languageId":"nix",
"version":1,
"text":"let\n pkgs = {\n a = 1;\n };\nin\nwith pkgs;\n\na\n\n"
}
}
}
```

File #2:


```json
{
"jsonrpc":"2.0",
"method":"textDocument/didOpen",
"params":{
"textDocument":{
"uri":"file:///parse-error.nix",
"languageId":"nix",
"version":1,
"text":"let x = 1; in y"
}
}
}
```

<-- textDocument/hover

```json
{
"jsonrpc":"2.0",
"id":2,
"method":"textDocument/hover",
"params":{
"textDocument":{
"uri":"file:///with.nix"
},
"position":{
"line":1,
"character":2
}
}
}
```

```
CHECK: "id": 2,
CHECK-NEXT: "jsonrpc": "2.0",
CHECK-NEXT: "result": {
CHECK-NEXT: "contents": {
CHECK-NEXT: "kind": "markdown",
CHECK-NEXT: "value": "`ExprAttrs`"
CHECK-NEXT: }
CHECK-NEXT: }
```

```json
{"jsonrpc":"2.0","method":"exit"}
```
84 changes: 84 additions & 0 deletions tools/nixd/test/hover-no-value.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# RUN: nixd --lit-test < %s | FileCheck %s

<-- initialize(0)

```json
{
"jsonrpc":"2.0",
"id":0,
"method":"initialize",
"params":{
"processId":123,
"rootPath":"",
"capabilities":{
},
"trace":"off"
}
}
```


<-- textDocument/didOpen

Testing this nix file:

```nix
let
pkgs = {
a = 1;
};
in
with pkgs;

a
```

```json
{
"jsonrpc":"2.0",
"method":"textDocument/didOpen",
"params":{
"textDocument":{
"uri":"file:///with.nix",
"languageId":"nix",
"version":1,
"text":"let\n pkgs = {\n a = 1;\n };\nin\nwith pkgs;\n\na\n\n"
}
}
}
```


<-- textDocument/hover

```json
{
"jsonrpc":"2.0",
"id":2,
"method":"textDocument/hover",
"params":{
"textDocument":{
"uri":"file:///with.nix"
},
"position":{
"line":1,
"character":2
}
}
}
```

```
CHECK: "id": 2,
CHECK-NEXT: "jsonrpc": "2.0",
CHECK-NEXT: "result": {
CHECK-NEXT: "contents": {
CHECK-NEXT: "kind": "markdown",
CHECK-NEXT: "value": "`ExprAttrs`"
CHECK-NEXT: }
CHECK-NEXT: }
```

```json
{"jsonrpc":"2.0","method":"exit"}
```
Loading