| // 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) { |
| if (!ShouldAddMessageOfKind(message.kind)) return; |
| |
| 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; |
| } |
| |
| bool ShouldAddMessageOfKind(TorqueMessage::Kind kind) { |
| // An error can easily cause a lot of false positive lint messages, due to |
| // unused variables, macros, etc. Thus we suppress subsequent lint messages |
| // when there are errors. |
| switch (kind) { |
| case TorqueMessage::Kind::kError: |
| suppress_lint_messages_ = true; |
| return true; |
| case TorqueMessage::Kind::kLint: |
| if (suppress_lint_messages_) return false; |
| return true; |
| } |
| } |
| |
| 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_; |
| bool suppress_lint_messages_ = false; |
| }; |
| |
| 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->IsGenericCallable()) { |
| GenericCallable* generic = GenericCallable::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 |