[mlir] Add hover support to mlir-lsp-server

This provides information when the user hovers over a part of the source .mlir file. This revision adds the following hover behavior:
* Operation:
  - Shows the generic form.
* Operation Result:
  - Shows the parent operation name, result number(s), and type(s).
* Block:
  - Shows the parent operation name, block number, predecessors, and successors.
* Block Argument:
  - Shows the parent operation name, parent block, argument number, and type.

Differential Revision: https://reviews.llvm.org/D101113
This commit is contained in:
River Riddle 2021-05-07 17:55:52 -07:00
parent ddff81f692
commit 5c84195b8c
10 changed files with 483 additions and 5 deletions

View File

@ -109,3 +109,6 @@ monorepo, a few extra steps for setup are required:
* Syntax highlighting for .mlir files and `mlir` markdown blocks
* go-to-definition and cross references
* Definitions include the source file locations of operations in the .mlir
* Hover over IR entities to see more information about them
* e.g. for a Block, you can see its block number as well as any
predecessors or successors.

View File

@ -95,6 +95,10 @@ public:
/// Return a range of the BlockDefinitions held by the current parser state.
iterator_range<BlockDefIterator> getBlockDefs() const;
/// Return the definition for the given block, or nullptr if the given
/// block does not have a definition.
const BlockDefinition *getBlockDef(Block *block) const;
/// Return a range of the OperationDefinitions held by the current parser
/// state.
iterator_range<OperationDefIterator> getOpDefs() const;

View File

@ -60,6 +60,12 @@ auto AsmParserState::getBlockDefs() const -> iterator_range<BlockDefIterator> {
return llvm::make_pointee_range(llvm::makeArrayRef(impl->blocks));
}
auto AsmParserState::getBlockDef(Block *block) const
-> const BlockDefinition * {
auto it = impl->blocksToIdx.find(block);
return it == impl->blocksToIdx.end() ? nullptr : &*impl->blocks[it->second];
}
auto AsmParserState::getOpDefs() const -> iterator_range<OperationDefIterator> {
return llvm::make_pointee_range(llvm::makeArrayRef(impl->operations));
}

View File

@ -50,6 +50,12 @@ struct LSPServer::Impl {
void onReference(const ReferenceParams &params,
Callback<std::vector<Location>> reply);
//===--------------------------------------------------------------------===//
// Hover
void onHover(const TextDocumentPositionParams &params,
Callback<Optional<Hover>> reply);
MLIRServer &server;
JSONTransport &transport;
@ -72,6 +78,7 @@ void LSPServer::Impl::onInitialize(const InitializeParams &params,
}},
{"definitionProvider", true},
{"referencesProvider", true},
{"hoverProvider", true},
};
llvm::json::Object result{
@ -125,6 +132,14 @@ void LSPServer::Impl::onReference(const ReferenceParams &params,
reply(std::move(locations));
}
//===----------------------------------------------------------------------===//
// Hover
void LSPServer::Impl::onHover(const TextDocumentPositionParams &params,
Callback<Optional<Hover>> reply) {
reply(server.findHover(params.textDocument.uri, params.position));
}
//===----------------------------------------------------------------------===//
// LSPServer
//===----------------------------------------------------------------------===//
@ -155,6 +170,10 @@ LogicalResult LSPServer::run() {
messageHandler.method("textDocument/references", impl.get(),
&Impl::onReference);
// Hover
messageHandler.method("textDocument/hover", impl.get(), &Impl::onHover);
// Run the main loop of the transport.
LogicalResult result = success();
if (llvm::Error error = impl->transport.run(messageHandler)) {
Logger::error("Transport error: {0}", error);

View File

@ -90,13 +90,87 @@ static bool contains(llvm::SMRange range, llvm::SMLoc loc) {
}
/// Returns true if the given location is contained by the definition or one of
/// the uses of the given SMDefinition.
static bool isDefOrUse(const AsmParserState::SMDefinition &def,
llvm::SMLoc loc) {
auto isUseFn = [&](const llvm::SMRange &range) {
/// the uses of the given SMDefinition. If provided, `overlappedRange` is set to
/// the range within `def` that the provided `loc` overlapped with.
static bool isDefOrUse(const AsmParserState::SMDefinition &def, llvm::SMLoc loc,
llvm::SMRange *overlappedRange = nullptr) {
// Check the main definition.
if (contains(def.loc, loc)) {
if (overlappedRange)
*overlappedRange = def.loc;
return true;
}
// Check the uses.
auto useIt = llvm::find_if(def.uses, [&](const llvm::SMRange &range) {
return contains(range, loc);
});
if (useIt != def.uses.end()) {
if (overlappedRange)
*overlappedRange = *useIt;
return true;
}
return false;
}
/// Given a location pointing to a result, return the result number it refers
/// to or None if it refers to all of the results.
static Optional<unsigned> getResultNumberFromLoc(llvm::SMLoc loc) {
// Skip all of the identifier characters.
auto isIdentifierChar = [](char c) {
return isalnum(c) || c == '%' || c == '$' || c == '.' || c == '_' ||
c == '-';
};
return contains(def.loc, loc) || llvm::any_of(def.uses, isUseFn);
const char *curPtr = loc.getPointer();
while (isIdentifierChar(*curPtr))
++curPtr;
// Check to see if this location indexes into the result group, via `#`. If it
// doesn't, we can't extract a sub result number.
if (*curPtr != '#')
return llvm::None;
// Compute the sub result number from the remaining portion of the string.
const char *numberStart = ++curPtr;
while (llvm::isDigit(*curPtr))
++curPtr;
StringRef numberStr(numberStart, curPtr - numberStart);
unsigned resultNumber = 0;
return numberStr.consumeInteger(10, resultNumber) ? Optional<unsigned>()
: resultNumber;
}
/// Given a source location range, return the text covered by the given range.
/// If the range is invalid, returns None.
static Optional<StringRef> getTextFromRange(llvm::SMRange range) {
if (!range.isValid())
return None;
const char *startPtr = range.Start.getPointer();
return StringRef(startPtr, range.End.getPointer() - startPtr);
}
/// Given a block, return its position in its parent region.
static unsigned getBlockNumber(Block *block) {
return std::distance(block->getParent()->begin(), block->getIterator());
}
/// Given a block and source location, print the source name of the block to the
/// given output stream.
static void printDefBlockName(raw_ostream &os, Block *block,
llvm::SMRange loc = {}) {
// Try to extract a name from the source location.
Optional<StringRef> text = getTextFromRange(loc);
if (text && text->startswith("^")) {
os << *text;
return;
}
// Otherwise, we don't have a name so print the block number.
os << "<Block #" << getBlockNumber(block) << ">";
}
static void printDefBlockName(raw_ostream &os,
const AsmParserState::BlockDefinition &def) {
printDefBlockName(os, def.block, def.definition.loc);
}
//===----------------------------------------------------------------------===//
@ -110,11 +184,33 @@ struct MLIRDocument {
MLIRDocument(const lsp::URIForFile &uri, StringRef contents,
DialectRegistry &registry);
//===--------------------------------------------------------------------===//
// Definitions and References
//===--------------------------------------------------------------------===//
void getLocationsOf(const lsp::URIForFile &uri, const lsp::Position &defPos,
std::vector<lsp::Location> &locations);
void findReferencesOf(const lsp::URIForFile &uri, const lsp::Position &pos,
std::vector<lsp::Location> &references);
//===--------------------------------------------------------------------===//
// Hover
//===--------------------------------------------------------------------===//
Optional<lsp::Hover> findHover(const lsp::URIForFile &uri,
const lsp::Position &hoverPos);
Optional<lsp::Hover>
buildHoverForOperation(const AsmParserState::OperationDefinition &op);
lsp::Hover buildHoverForOperationResult(llvm::SMRange hoverRange,
Operation *op, unsigned resultStart,
unsigned resultEnd,
llvm::SMLoc posLoc);
lsp::Hover buildHoverForBlock(llvm::SMRange hoverRange,
const AsmParserState::BlockDefinition &block);
lsp::Hover
buildHoverForBlockArgument(llvm::SMRange hoverRange, BlockArgument arg,
const AsmParserState::BlockDefinition &block);
/// The context used to hold the state contained by the parsed document.
MLIRContext context;
@ -155,6 +251,10 @@ MLIRDocument::MLIRDocument(const lsp::URIForFile &uri, StringRef contents,
return;
}
//===----------------------------------------------------------------------===//
// MLIRDocument: Definitions and References
//===----------------------------------------------------------------------===//
void MLIRDocument::getLocationsOf(const lsp::URIForFile &uri,
const lsp::Position &defPos,
std::vector<lsp::Location> &locations) {
@ -223,6 +323,155 @@ void MLIRDocument::findReferencesOf(const lsp::URIForFile &uri,
}
}
//===----------------------------------------------------------------------===//
// MLIRDocument: Hover
//===----------------------------------------------------------------------===//
Optional<lsp::Hover> MLIRDocument::findHover(const lsp::URIForFile &uri,
const lsp::Position &hoverPos) {
llvm::SMLoc posLoc = getPosFromLoc(sourceMgr, hoverPos);
llvm::SMRange hoverRange;
// Check for Hovers on operations and results.
for (const AsmParserState::OperationDefinition &op : asmState.getOpDefs()) {
// Check if the position points at this operation.
if (contains(op.loc, posLoc))
return buildHoverForOperation(op);
// Check if the position points at a result group.
for (unsigned i = 0, e = op.resultGroups.size(); i < e; ++i) {
const auto &result = op.resultGroups[i];
if (!isDefOrUse(result.second, posLoc, &hoverRange))
continue;
// Get the range of results covered by the over position.
unsigned resultStart = result.first;
unsigned resultEnd =
(i == e - 1) ? op.op->getNumResults() : op.resultGroups[i + 1].first;
return buildHoverForOperationResult(hoverRange, op.op, resultStart,
resultEnd, posLoc);
}
}
// Check to see if the hover is over a block argument.
for (const AsmParserState::BlockDefinition &block : asmState.getBlockDefs()) {
if (isDefOrUse(block.definition, posLoc, &hoverRange))
return buildHoverForBlock(hoverRange, block);
for (const auto &arg : llvm::enumerate(block.arguments)) {
if (!isDefOrUse(arg.value(), posLoc, &hoverRange))
continue;
return buildHoverForBlockArgument(
hoverRange, block.block->getArgument(arg.index()), block);
}
}
return llvm::None;
}
Optional<lsp::Hover> MLIRDocument::buildHoverForOperation(
const AsmParserState::OperationDefinition &op) {
// Don't show hovers for operations with regions to avoid huge hover blocks.
// TODO: Should we add support for printing an op without its regions?
if (llvm::any_of(op.op->getRegions(),
[](Region &region) { return !region.empty(); }))
return llvm::None;
lsp::Hover hover(getRangeFromLoc(sourceMgr, op.loc));
llvm::raw_string_ostream os(hover.contents.value);
// For hovers on an operation, show the generic form.
os << "```mlir\n";
op.op->print(
os, OpPrintingFlags().printGenericOpForm().elideLargeElementsAttrs());
os << "\n```\n";
return hover;
}
lsp::Hover MLIRDocument::buildHoverForOperationResult(llvm::SMRange hoverRange,
Operation *op,
unsigned resultStart,
unsigned resultEnd,
llvm::SMLoc posLoc) {
lsp::Hover hover(getRangeFromLoc(sourceMgr, hoverRange));
llvm::raw_string_ostream os(hover.contents.value);
// Add the parent operation name to the hover.
os << "Operation: \"" << op->getName() << "\"\n\n";
// Check to see if the location points to a specific result within the
// group.
if (Optional<unsigned> resultNumber = getResultNumberFromLoc(posLoc)) {
if ((resultStart + *resultNumber) < resultEnd) {
resultStart += *resultNumber;
resultEnd = resultStart + 1;
}
}
// Add the range of results and their types to the hover info.
if ((resultStart + 1) == resultEnd) {
os << "Result #" << resultStart << "\n\n"
<< "Type: `" << op->getResult(resultStart).getType() << "`\n\n";
} else {
os << "Result #[" << resultStart << ", " << (resultEnd - 1) << "]\n\n"
<< "Types: ";
llvm::interleaveComma(
op->getResults().slice(resultStart, resultEnd), os,
[&](Value result) { os << "`" << result.getType() << "`"; });
}
return hover;
}
lsp::Hover
MLIRDocument::buildHoverForBlock(llvm::SMRange hoverRange,
const AsmParserState::BlockDefinition &block) {
lsp::Hover hover(getRangeFromLoc(sourceMgr, hoverRange));
llvm::raw_string_ostream os(hover.contents.value);
// Print the given block to the hover output stream.
auto printBlockToHover = [&](Block *newBlock) {
if (const auto *def = asmState.getBlockDef(newBlock))
printDefBlockName(os, *def);
else
printDefBlockName(os, newBlock);
};
// Display the parent operation, block number, predecessors, and successors.
os << "Operation: \"" << block.block->getParentOp()->getName() << "\"\n\n"
<< "Block #" << getBlockNumber(block.block) << "\n\n";
if (!block.block->hasNoPredecessors()) {
os << "Predecessors: ";
llvm::interleaveComma(block.block->getPredecessors(), os,
printBlockToHover);
os << "\n\n";
}
if (!block.block->hasNoSuccessors()) {
os << "Successors: ";
llvm::interleaveComma(block.block->getSuccessors(), os, printBlockToHover);
os << "\n\n";
}
return hover;
}
lsp::Hover MLIRDocument::buildHoverForBlockArgument(
llvm::SMRange hoverRange, BlockArgument arg,
const AsmParserState::BlockDefinition &block) {
lsp::Hover hover(getRangeFromLoc(sourceMgr, hoverRange));
llvm::raw_string_ostream os(hover.contents.value);
// Display the parent operation, block, the argument number, and the type.
os << "Operation: \"" << block.block->getParentOp()->getName() << "\"\n\n"
<< "Block: ";
printDefBlockName(os, block);
os << "\n\nArgument #" << arg.getArgNumber() << "\n\n"
<< "Type: `" << arg.getType() << "`\n\n";
return hover;
}
//===----------------------------------------------------------------------===//
// MLIRServer::Impl
//===----------------------------------------------------------------------===//
@ -271,3 +520,11 @@ void lsp::MLIRServer::findReferencesOf(const URIForFile &uri,
if (fileIt != impl->documents.end())
fileIt->second->findReferencesOf(uri, pos, references);
}
Optional<lsp::Hover> lsp::MLIRServer::findHover(const URIForFile &uri,
const Position &hoverPos) {
auto fileIt = impl->documents.find(uri.file());
if (fileIt != impl->documents.end())
return fileIt->second->findHover(uri, hoverPos);
return llvm::None;
}

View File

@ -16,6 +16,7 @@ namespace mlir {
class DialectRegistry;
namespace lsp {
struct Hover;
struct Location;
struct Position;
class URIForFile;
@ -43,6 +44,10 @@ public:
void findReferencesOf(const URIForFile &uri, const Position &pos,
std::vector<Location> &references);
/// Find a hover description for the given hover position, or None if one
/// couldn't be found.
Optional<Hover> findHover(const URIForFile &uri, const Position &hoverPos);
private:
struct Impl;

View File

@ -434,3 +434,42 @@ bool mlir::lsp::fromJSON(const llvm::json::Value &value,
return o && o.map("textDocument", result.textDocument) &&
o.map("contentChanges", result.contentChanges);
}
//===----------------------------------------------------------------------===//
// MarkupContent
//===----------------------------------------------------------------------===//
static llvm::StringRef toTextKind(MarkupKind kind) {
switch (kind) {
case MarkupKind::PlainText:
return "plaintext";
case MarkupKind::Markdown:
return "markdown";
}
llvm_unreachable("Invalid MarkupKind");
}
raw_ostream &mlir::lsp::operator<<(raw_ostream &os, MarkupKind kind) {
return os << toTextKind(kind);
}
llvm::json::Value mlir::lsp::toJSON(const MarkupContent &mc) {
if (mc.value.empty())
return nullptr;
return llvm::json::Object{
{"kind", toTextKind(mc.kind)},
{"value", mc.value},
};
}
//===----------------------------------------------------------------------===//
// Hover
//===----------------------------------------------------------------------===//
llvm::json::Value mlir::lsp::toJSON(const Hover &hover) {
llvm::json::Object result{{"contents", toJSON(hover.contents)}};
if (hover.range.hasValue())
result["range"] = toJSON(*hover.range);
return std::move(result);
}

View File

@ -358,6 +358,41 @@ struct DidChangeTextDocumentParams {
bool fromJSON(const llvm::json::Value &value,
DidChangeTextDocumentParams &result, llvm::json::Path path);
//===----------------------------------------------------------------------===//
// MarkupContent
//===----------------------------------------------------------------------===//
/// Describes the content type that a client supports in various result literals
/// like `Hover`.
enum class MarkupKind {
PlainText,
Markdown,
};
raw_ostream &operator<<(raw_ostream &os, MarkupKind kind);
struct MarkupContent {
MarkupKind kind = MarkupKind::PlainText;
std::string value;
};
llvm::json::Value toJSON(const MarkupContent &mc);
//===----------------------------------------------------------------------===//
// Hover
//===----------------------------------------------------------------------===//
struct Hover {
/// Construct a default hover with the given range that uses Markdown content.
Hover(Range range) : contents{MarkupKind::Markdown, ""}, range(range) {}
/// The hover's content.
MarkupContent contents;
/// An optional range is a range inside a text document that is used to
/// visualize a hover, e.g. by changing the background color.
Optional<Range> range;
};
llvm::json::Value toJSON(const Hover &hover);
} // namespace lsp
} // namespace mlir

View File

@ -0,0 +1,109 @@
// RUN: mlir-lsp-server -lit-test < %s | FileCheck -strict-whitespace %s
{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"mlir","capabilities":{},"trace":"off"}}
// -----
{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{
"uri":"test:///foo.mlir",
"languageId":"mlir",
"version":1,
"text":"func @foo(%arg: i1) {\n%value = constant true\nbr ^bb2\n^bb2:\nreturn\n}"
}}}
// -----
// Hover on an operation.
{"jsonrpc":"2.0","id":1,"method":"textDocument/hover","params":{
"textDocument":{"uri":"test:///foo.mlir"},
"position":{"line":1,"character":12}
}}
// CHECK: "id": 1,
// CHECK-NEXT: "jsonrpc": "2.0",
// CHECK-NEXT: "result": {
// CHECK-NEXT: "contents": {
// CHECK-NEXT: "kind": "markdown",
// CHECK-NEXT: "value": "```mlir\n%true = \"std.constant\"() {value = true} : () -> i1\n```\n"
// CHECK-NEXT: },
// CHECK-NEXT: "range": {
// CHECK-NEXT: "end": {
// CHECK-NEXT: "character": 17,
// CHECK-NEXT: "line": 1
// CHECK-NEXT: },
// CHECK-NEXT: "start": {
// CHECK-NEXT: "character": 10,
// CHECK-NEXT: "line": 1
// CHECK-NEXT: }
// CHECK-NEXT: }
// CHECK-NEXT: }
// -----
// Hover on an operation result.
{"jsonrpc":"2.0","id":1,"method":"textDocument/hover","params":{
"textDocument":{"uri":"test:///foo.mlir"},
"position":{"line":1,"character":2}
}}
// CHECK: "id": 1,
// CHECK-NEXT: "jsonrpc": "2.0",
// CHECK-NEXT: "result": {
// CHECK-NEXT: "contents": {
// CHECK-NEXT: "kind": "markdown",
// CHECK-NEXT: "value": "Operation: \"std.constant\"\n\nResult #0\n\nType: `i1`\n\n"
// CHECK-NEXT: },
// CHECK-NEXT: "range": {
// CHECK-NEXT: "end": {
// CHECK-NEXT: "character": 6,
// CHECK-NEXT: "line": 1
// CHECK-NEXT: },
// CHECK-NEXT: "start": {
// CHECK-NEXT: "character": 1,
// CHECK-NEXT: "line": 1
// CHECK-NEXT: }
// CHECK-NEXT: }
// CHECK-NEXT: }
// -----
// Hover on a Block.
{"jsonrpc":"2.0","id":1,"method":"textDocument/hover","params":{
"textDocument":{"uri":"test:///foo.mlir"},
"position":{"line":3,"character":2}
}}
// CHECK: "id": 1,
// CHECK-NEXT: "jsonrpc": "2.0",
// CHECK-NEXT: "result": {
// CHECK-NEXT: "contents": {
// CHECK-NEXT: "kind": "markdown",
// CHECK-NEXT: "value": "Operation: \"func\"\n\nBlock #1\n\nPredecessors: <Block #0>\n\n"
// CHECK-NEXT: },
// CHECK-NEXT: "range": {
// CHECK-NEXT: "end": {
// CHECK-NEXT: "character": 4,
// CHECK-NEXT: "line": 3
// CHECK-NEXT: },
// CHECK-NEXT: "start": {
// CHECK-NEXT: "character": 1,
// CHECK-NEXT: "line": 3
// CHECK-NEXT: }
// CHECK-NEXT: }
// CHECK-NEXT: }
// -----
// Hover on a Block argument.
{"jsonrpc":"2.0","id":1,"method":"textDocument/hover","params":{
"textDocument":{"uri":"test:///foo.mlir"},
"position":{"line":0,"character":12}
}}
// CHECK: "id": 1,
// CHECK-NEXT: "jsonrpc": "2.0",
// CHECK-NEXT: "result": {
// CHECK-NEXT: "contents": {
// CHECK-NEXT: "kind": "markdown",
// CHECK-NEXT: "value": "Operation: \"func\"\n\nBlock: <Block #0>\n\nArgument #0\n\nType: `i1`\n\n"
// CHECK-NEXT: },
// CHECK-NEXT: "range": {
// CHECK-NEXT: "end": {
// CHECK-NEXT: "character": 14,
// CHECK-NEXT: "line": 0
// CHECK-NEXT: },
// CHECK-NEXT: "start": {
// CHECK-NEXT: "character": 11,
// CHECK-NEXT: "line": 0
// CHECK-NEXT: }
// CHECK-NEXT: }
// CHECK-NEXT: }
// -----
{"jsonrpc":"2.0","id":3,"method":"shutdown"}
// -----
{"jsonrpc":"2.0","method":"exit"}

View File

@ -6,6 +6,7 @@
// CHECK-NEXT: "result": {
// CHECK-NEXT: "capabilities": {
// CHECK-NEXT: "definitionProvider": true,
// CHECK-NEXT: "hoverProvider": true,
// CHECK-NEXT: "referencesProvider": true,
// CHECK-NEXT: "textDocumentSync": {
// CHECK-NEXT: "change": 1,