blob: 6ec124b5a26b80d2fd84c09977d5d84c8ed4bc82 [file] [log] [blame]
// Copyright 2019 the V8 project authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <algorithm>
#include "src/torque/ls/message-handler.h"
#include "src/torque/ls/globals.h"
#include "src/torque/ls/json-parser.h"
#include "src/torque/ls/message-pipe.h"
#include "src/torque/ls/message.h"
#include "src/torque/server-data.h"
#include "src/torque/source-positions.h"
#include "src/torque/torque-compiler.h"
namespace v8 {
namespace internal {
namespace torque {
DEFINE_CONTEXTUAL_VARIABLE(Logger)
DEFINE_CONTEXTUAL_VARIABLE(TorqueFileList)
DEFINE_CONTEXTUAL_VARIABLE(DiagnosticsFiles)
namespace ls {
static const char kContentLength[] = "Content-Length: ";
static const size_t kContentLengthSize = sizeof(kContentLength) - 1;
#ifdef V8_OS_WIN
// On Windows, in text mode, \n is translated to \r\n.
constexpr const char* kProtocolLineEnding = "\n\n";
#else
constexpr const char* kProtocolLineEnding = "\r\n\r\n";
#endif
JsonValue ReadMessage() {
std::string line;
std::getline(std::cin, line);
if (line.rfind(kContentLength) != 0) {
// Invalid message, we just crash.
Logger::Log("[fatal] Did not find Content-Length ...\n");
v8::base::OS::Abort();
}
const int content_length = std::atoi(line.substr(kContentLengthSize).c_str());
std::getline(std::cin, line);
std::string content(content_length, ' ');
std::cin.read(&content[0], content_length);
Logger::Log("[incoming] ", content, "\n\n");
return ParseJson(content).value;
}
void WriteMessage(JsonValue message) {
std::string content = SerializeToString(message);
Logger::Log("[outgoing] ", content, "\n\n");
std::cout << kContentLength << content.size() << kProtocolLineEnding;
std::cout << content << std::flush;
}
namespace {
void ResetCompilationErrorDiagnostics(MessageWriter writer) {
for (const SourceId& source : DiagnosticsFiles::Get()) {
PublishDiagnosticsNotification notification;
notification.set_method("textDocument/publishDiagnostics");
std::string error_file = SourceFileMap::AbsolutePath(source);
notification.params().set_uri(error_file);
// Trigger empty array creation.
USE(notification.params().diagnostics_size());
writer(std::move(notification.GetJsonValue()));
}
DiagnosticsFiles::Get() = {};
}
// Each notification must contain all diagnostics for a specific file,
// because sending multiple notifications per file resets previously sent
// diagnostics. Thus, two steps are needed:
// 1) collect all notifications in this class.
// 2) send one notification per entry (per file).
class DiagnosticCollector {
public:
void AddTorqueMessage(const TorqueMessage& message) {
SourceId id =
message.position ? message.position->source : SourceId::Invalid();
auto& notification = GetOrCreateNotificationForSource(id);
Diagnostic diagnostic = notification.params().add_diagnostics();
diagnostic.set_severity(ServerityFor(message.kind));
diagnostic.set_message(message.message);
diagnostic.set_source("Torque Compiler");
if (message.position) {
PopulateRangeFromSourcePosition(diagnostic.range(), *message.position);
}
}
std::map<SourceId, PublishDiagnosticsNotification>& notifications() {
return notifications_;
}
private:
PublishDiagnosticsNotification& GetOrCreateNotificationForSource(
SourceId id) {
auto iter = notifications_.find(id);
if (iter != notifications_.end()) return iter->second;
PublishDiagnosticsNotification& notification = notifications_[id];
notification.set_method("textDocument/publishDiagnostics");
std::string file =
id.IsValid() ? SourceFileMap::AbsolutePath(id) : "<unknown>";
notification.params().set_uri(file);
return notification;
}
void PopulateRangeFromSourcePosition(Range range,
const SourcePosition& position) {
range.start().set_line(position.start.line);
range.start().set_character(position.start.column);
range.end().set_line(position.end.line);
range.end().set_character(position.end.column);
}
Diagnostic::DiagnosticSeverity ServerityFor(TorqueMessage::Kind kind) {
switch (kind) {
case TorqueMessage::Kind::kError:
return Diagnostic::kError;
case TorqueMessage::Kind::kLint:
return Diagnostic::kWarning;
}
}
std::map<SourceId, PublishDiagnosticsNotification> notifications_;
};
void SendCompilationDiagnostics(const TorqueCompilerResult& result,
MessageWriter writer) {
DiagnosticCollector collector;
// TODO(szuend): Split up messages by SourceId and sort them by line number.
for (const TorqueMessage& message : result.messages) {
collector.AddTorqueMessage(message);
}
for (auto& pair : collector.notifications()) {
PublishDiagnosticsNotification& notification = pair.second;
writer(std::move(notification.GetJsonValue()));
// Record all source files for which notifications are sent, so they
// can be reset before the next compiler run.
const SourceId& source = pair.first;
if (source.IsValid()) DiagnosticsFiles::Get().push_back(source);
}
}
} // namespace
void CompilationFinished(TorqueCompilerResult result, MessageWriter writer) {
LanguageServerData::Get() = std::move(result.language_server_data);
SourceFileMap::Get() = *result.source_file_map;
SendCompilationDiagnostics(result, writer);
}
namespace {
void RecompileTorque(MessageWriter writer) {
Logger::Log("[info] Start compilation run ...\n");
TorqueCompilerOptions options;
options.output_directory = "";
options.collect_language_server_data = true;
options.force_assert_statements = true;
TorqueCompilerResult result = CompileTorque(TorqueFileList::Get(), options);
Logger::Log("[info] Finished compilation run ...\n");
CompilationFinished(std::move(result), writer);
}
void RecompileTorqueWithDiagnostics(MessageWriter writer) {
ResetCompilationErrorDiagnostics(writer);
RecompileTorque(writer);
}
void HandleInitializeRequest(InitializeRequest request, MessageWriter writer) {
InitializeResponse response;
response.set_id(request.id());
response.result().capabilities().textDocumentSync();
response.result().capabilities().set_definitionProvider(true);
response.result().capabilities().set_documentSymbolProvider(true);
// TODO(szuend): Register for document synchronisation here,
// so we work with the content that the client
// provides, not directly read from files.
// TODO(szuend): Check that the client actually supports dynamic
// "workspace/didChangeWatchedFiles" capability.
// TODO(szuend): Check if client supports "LocationLink". This will
// influence the result of "goto definition".
writer(std::move(response.GetJsonValue()));
}
void HandleInitializedNotification(MessageWriter writer) {
RegistrationRequest request;
// TODO(szuend): The language server needs a "global" request id counter.
request.set_id(2000);
request.set_method("client/registerCapability");
Registration reg = request.params().add_registrations();
auto options =
reg.registerOptions<DidChangeWatchedFilesRegistrationOptions>();
FileSystemWatcher watcher = options.add_watchers();
watcher.set_globPattern("**/*.tq");
watcher.set_kind(FileSystemWatcher::WatchKind::kAll);
reg.set_id("did-change-id");
reg.set_method("workspace/didChangeWatchedFiles");
writer(std::move(request.GetJsonValue()));
}
void HandleTorqueFileListNotification(TorqueFileListNotification notification,
MessageWriter writer) {
CHECK_EQ(notification.params().object()["files"].tag, JsonValue::ARRAY);
std::vector<std::string>& files = TorqueFileList::Get();
Logger::Log("[info] Initial file list:\n");
for (const auto& file_json :
notification.params().object()["files"].ToArray()) {
CHECK(file_json.IsString());
// We only consider file URIs (there shouldn't be anything else).
// Internally we store the URI instead of the path, eliminating the need
// to encode it again.
files.push_back(file_json.ToString());
Logger::Log(" ", file_json.ToString(), "\n");
}
RecompileTorqueWithDiagnostics(writer);
}
void HandleGotoDefinitionRequest(GotoDefinitionRequest request,
MessageWriter writer) {
GotoDefinitionResponse response;
response.set_id(request.id());
SourceId id =
SourceFileMap::GetSourceId(request.params().textDocument().uri());
// Unknown source files cause an empty response which corresponds with
// the definition not beeing found.
if (!id.IsValid()) {
response.SetNull("result");
writer(std::move(response.GetJsonValue()));
return;
}
LineAndColumn pos{request.params().position().line(),
request.params().position().character()};
if (auto maybe_definition = LanguageServerData::FindDefinition(id, pos)) {
SourcePosition definition = *maybe_definition;
response.result().SetTo(definition);
} else {
response.SetNull("result");
}
writer(std::move(response.GetJsonValue()));
}
void HandleChangeWatchedFilesNotification(
DidChangeWatchedFilesNotification notification, MessageWriter writer) {
// TODO(szuend): Implement updates to the TorqueFile list when create/delete
// notifications are received. Currently we simply re-compile.
RecompileTorqueWithDiagnostics(writer);
}
void HandleDocumentSymbolRequest(DocumentSymbolRequest request,
MessageWriter writer) {
DocumentSymbolResponse response;
response.set_id(request.id());
SourceId id =
SourceFileMap::GetSourceId(request.params().textDocument().uri());
for (const auto& symbol : LanguageServerData::SymbolsForSourceId(id)) {
DCHECK(symbol->IsUserDefined());
if (symbol->IsMacro()) {
Macro* macro = Macro::cast(symbol);
SymbolInformation symbol = response.add_result();
symbol.set_name(macro->ReadableName());
symbol.set_kind(SymbolKind::kFunction);
symbol.location().SetTo(macro->Position());
} else if (symbol->IsBuiltin()) {
Builtin* builtin = Builtin::cast(symbol);
SymbolInformation symbol = response.add_result();
symbol.set_name(builtin->ReadableName());
symbol.set_kind(SymbolKind::kFunction);
symbol.location().SetTo(builtin->Position());
} else if (symbol->IsGeneric()) {
Generic* generic = Generic::cast(symbol);
SymbolInformation symbol = response.add_result();
symbol.set_name(generic->name());
symbol.set_kind(SymbolKind::kFunction);
symbol.location().SetTo(generic->Position());
} else if (symbol->IsTypeAlias()) {
const Type* type = TypeAlias::cast(symbol)->type();
SymbolKind kind =
type->IsClassType() ? SymbolKind::kClass : SymbolKind::kStruct;
SymbolInformation sym = response.add_result();
sym.set_name(type->ToString());
sym.set_kind(kind);
sym.location().SetTo(symbol->Position());
}
}
// Trigger empty array creation in case no symbols were found.
USE(response.result_size());
writer(std::move(response.GetJsonValue()));
}
} // namespace
void HandleMessage(JsonValue raw_message, MessageWriter writer) {
Request<bool> request(std::move(raw_message));
// We ignore responses for now. They are matched to requests
// by id and don't have a method set.
// TODO(szuend): Implement proper response handling for requests
// that originate from the server.
if (!request.has_method()) {
Logger::Log("[info] Unhandled response with id ", request.id(), "\n\n");
return;
}
const std::string method = request.method();
if (method == "initialize") {
HandleInitializeRequest(
InitializeRequest(std::move(request.GetJsonValue())), writer);
} else if (method == "initialized") {
HandleInitializedNotification(writer);
} else if (method == "torque/fileList") {
HandleTorqueFileListNotification(
TorqueFileListNotification(std::move(request.GetJsonValue())), writer);
} else if (method == "textDocument/definition") {
HandleGotoDefinitionRequest(
GotoDefinitionRequest(std::move(request.GetJsonValue())), writer);
} else if (method == "workspace/didChangeWatchedFiles") {
HandleChangeWatchedFilesNotification(
DidChangeWatchedFilesNotification(std::move(request.GetJsonValue())),
writer);
} else if (method == "textDocument/documentSymbol") {
HandleDocumentSymbolRequest(
DocumentSymbolRequest(std::move(request.GetJsonValue())), writer);
} else {
Logger::Log("[error] Message of type ", method, " is not handled!\n\n");
}
}
} // namespace ls
} // namespace torque
} // namespace internal
} // namespace v8