1// Copyright 2019 the V8 project authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include <algorithm> 6#include "src/torque/ls/message-handler.h" 7 8#include "src/torque/ls/globals.h" 9#include "src/torque/ls/json-parser.h" 10#include "src/torque/ls/message-pipe.h" 11#include "src/torque/ls/message.h" 12#include "src/torque/server-data.h" 13#include "src/torque/source-positions.h" 14#include "src/torque/torque-compiler.h" 15 16namespace v8 { 17namespace internal { 18namespace torque { 19 20DEFINE_CONTEXTUAL_VARIABLE(Logger) 21DEFINE_CONTEXTUAL_VARIABLE(TorqueFileList) 22DEFINE_CONTEXTUAL_VARIABLE(DiagnosticsFiles) 23 24namespace ls { 25 26static const char kContentLength[] = "Content-Length: "; 27static const size_t kContentLengthSize = sizeof(kContentLength) - 1; 28 29#ifdef V8_OS_WIN 30// On Windows, in text mode, \n is translated to \r\n. 31constexpr const char* kProtocolLineEnding = "\n\n"; 32#else 33constexpr const char* kProtocolLineEnding = "\r\n\r\n"; 34#endif 35 36JsonValue ReadMessage() { 37 std::string line; 38 std::getline(std::cin, line); 39 40 if (line.rfind(kContentLength) != 0) { 41 // Invalid message, we just crash. 42 Logger::Log("[fatal] Did not find Content-Length ...\n"); 43 v8::base::OS::Abort(); 44 } 45 46 const int content_length = std::atoi(line.substr(kContentLengthSize).c_str()); 47 std::getline(std::cin, line); 48 std::string content(content_length, ' '); 49 std::cin.read(&content[0], content_length); 50 51 Logger::Log("[incoming] ", content, "\n\n"); 52 53 return ParseJson(content).value; 54} 55 56void WriteMessage(JsonValue message) { 57 std::string content = SerializeToString(message); 58 59 Logger::Log("[outgoing] ", content, "\n\n"); 60 61 std::cout << kContentLength << content.size() << kProtocolLineEnding; 62 std::cout << content << std::flush; 63} 64 65namespace { 66 67void ResetCompilationErrorDiagnostics(MessageWriter writer) { 68 for (const SourceId& source : DiagnosticsFiles::Get()) { 69 PublishDiagnosticsNotification notification; 70 notification.set_method("textDocument/publishDiagnostics"); 71 72 std::string error_file = SourceFileMap::AbsolutePath(source); 73 notification.params().set_uri(error_file); 74 // Trigger empty array creation. 75 USE(notification.params().diagnostics_size()); 76 77 writer(std::move(notification.GetJsonValue())); 78 } 79 DiagnosticsFiles::Get() = {}; 80} 81 82// Each notification must contain all diagnostics for a specific file, 83// because sending multiple notifications per file resets previously sent 84// diagnostics. Thus, two steps are needed: 85// 1) collect all notifications in this class. 86// 2) send one notification per entry (per file). 87class DiagnosticCollector { 88 public: 89 void AddTorqueMessage(const TorqueMessage& message) { 90 if (!ShouldAddMessageOfKind(message.kind)) return; 91 92 SourceId id = 93 message.position ? message.position->source : SourceId::Invalid(); 94 auto& notification = GetOrCreateNotificationForSource(id); 95 96 Diagnostic diagnostic = notification.params().add_diagnostics(); 97 diagnostic.set_severity(ServerityFor(message.kind)); 98 diagnostic.set_message(message.message); 99 diagnostic.set_source("Torque Compiler"); 100 101 if (message.position) { 102 PopulateRangeFromSourcePosition(diagnostic.range(), *message.position); 103 } 104 } 105 106 std::map<SourceId, PublishDiagnosticsNotification>& notifications() { 107 return notifications_; 108 } 109 110 private: 111 PublishDiagnosticsNotification& GetOrCreateNotificationForSource( 112 SourceId id) { 113 auto iter = notifications_.find(id); 114 if (iter != notifications_.end()) return iter->second; 115 116 PublishDiagnosticsNotification& notification = notifications_[id]; 117 notification.set_method("textDocument/publishDiagnostics"); 118 119 std::string file = 120 id.IsValid() ? SourceFileMap::AbsolutePath(id) : "<unknown>"; 121 notification.params().set_uri(file); 122 return notification; 123 } 124 125 bool ShouldAddMessageOfKind(TorqueMessage::Kind kind) { 126 // An error can easily cause a lot of false positive lint messages, due to 127 // unused variables, macros, etc. Thus we suppress subsequent lint messages 128 // when there are errors. 129 switch (kind) { 130 case TorqueMessage::Kind::kError: 131 suppress_lint_messages_ = true; 132 return true; 133 case TorqueMessage::Kind::kLint: 134 if (suppress_lint_messages_) return false; 135 return true; 136 } 137 } 138 139 void PopulateRangeFromSourcePosition(Range range, 140 const SourcePosition& position) { 141 range.start().set_line(position.start.line); 142 range.start().set_character(position.start.column); 143 range.end().set_line(position.end.line); 144 range.end().set_character(position.end.column); 145 } 146 147 Diagnostic::DiagnosticSeverity ServerityFor(TorqueMessage::Kind kind) { 148 switch (kind) { 149 case TorqueMessage::Kind::kError: 150 return Diagnostic::kError; 151 case TorqueMessage::Kind::kLint: 152 return Diagnostic::kWarning; 153 } 154 } 155 156 std::map<SourceId, PublishDiagnosticsNotification> notifications_; 157 bool suppress_lint_messages_ = false; 158}; 159 160void SendCompilationDiagnostics(const TorqueCompilerResult& result, 161 MessageWriter writer) { 162 DiagnosticCollector collector; 163 164 // TODO(szuend): Split up messages by SourceId and sort them by line number. 165 for (const TorqueMessage& message : result.messages) { 166 collector.AddTorqueMessage(message); 167 } 168 169 for (auto& pair : collector.notifications()) { 170 PublishDiagnosticsNotification& notification = pair.second; 171 writer(std::move(notification.GetJsonValue())); 172 173 // Record all source files for which notifications are sent, so they 174 // can be reset before the next compiler run. 175 const SourceId& source = pair.first; 176 if (source.IsValid()) DiagnosticsFiles::Get().push_back(source); 177 } 178} 179 180} // namespace 181 182void CompilationFinished(TorqueCompilerResult result, MessageWriter writer) { 183 LanguageServerData::Get() = std::move(result.language_server_data); 184 SourceFileMap::Get() = *result.source_file_map; 185 186 SendCompilationDiagnostics(result, writer); 187} 188 189namespace { 190 191void RecompileTorque(MessageWriter writer) { 192 Logger::Log("[info] Start compilation run ...\n"); 193 194 TorqueCompilerOptions options; 195 options.output_directory = ""; 196 options.collect_language_server_data = true; 197 options.force_assert_statements = true; 198 199 TorqueCompilerResult result = CompileTorque(TorqueFileList::Get(), options); 200 201 Logger::Log("[info] Finished compilation run ...\n"); 202 203 CompilationFinished(std::move(result), writer); 204} 205 206void RecompileTorqueWithDiagnostics(MessageWriter writer) { 207 ResetCompilationErrorDiagnostics(writer); 208 RecompileTorque(writer); 209} 210 211void HandleInitializeRequest(InitializeRequest request, MessageWriter writer) { 212 InitializeResponse response; 213 response.set_id(request.id()); 214 response.result().capabilities().textDocumentSync(); 215 response.result().capabilities().set_definitionProvider(true); 216 response.result().capabilities().set_documentSymbolProvider(true); 217 218 // TODO(szuend): Register for document synchronisation here, 219 // so we work with the content that the client 220 // provides, not directly read from files. 221 // TODO(szuend): Check that the client actually supports dynamic 222 // "workspace/didChangeWatchedFiles" capability. 223 // TODO(szuend): Check if client supports "LocationLink". This will 224 // influence the result of "goto definition". 225 writer(std::move(response.GetJsonValue())); 226} 227 228void HandleInitializedNotification(MessageWriter writer) { 229 RegistrationRequest request; 230 // TODO(szuend): The language server needs a "global" request id counter. 231 request.set_id(2000); 232 request.set_method("client/registerCapability"); 233 234 Registration reg = request.params().add_registrations(); 235 auto options = 236 reg.registerOptions<DidChangeWatchedFilesRegistrationOptions>(); 237 FileSystemWatcher watcher = options.add_watchers(); 238 watcher.set_globPattern("**/*.tq"); 239 watcher.set_kind(FileSystemWatcher::WatchKind::kAll); 240 241 reg.set_id("did-change-id"); 242 reg.set_method("workspace/didChangeWatchedFiles"); 243 244 writer(std::move(request.GetJsonValue())); 245} 246 247void HandleTorqueFileListNotification(TorqueFileListNotification notification, 248 MessageWriter writer) { 249 CHECK_EQ(notification.params().object()["files"].tag, JsonValue::ARRAY); 250 251 std::vector<std::string>& files = TorqueFileList::Get(); 252 Logger::Log("[info] Initial file list:\n"); 253 for (const auto& file_json : 254 notification.params().object()["files"].ToArray()) { 255 CHECK(file_json.IsString()); 256 257 // We only consider file URIs (there shouldn't be anything else). 258 // Internally we store the URI instead of the path, eliminating the need 259 // to encode it again. 260 files.push_back(file_json.ToString()); 261 Logger::Log(" ", file_json.ToString(), "\n"); 262 } 263 RecompileTorqueWithDiagnostics(writer); 264} 265 266void HandleGotoDefinitionRequest(GotoDefinitionRequest request, 267 MessageWriter writer) { 268 GotoDefinitionResponse response; 269 response.set_id(request.id()); 270 271 SourceId id = 272 SourceFileMap::GetSourceId(request.params().textDocument().uri()); 273 274 // Unknown source files cause an empty response which corresponds with 275 // the definition not beeing found. 276 if (!id.IsValid()) { 277 response.SetNull("result"); 278 writer(std::move(response.GetJsonValue())); 279 return; 280 } 281 282 auto pos = 283 LineAndColumn::WithUnknownOffset(request.params().position().line(), 284 request.params().position().character()); 285 286 if (auto maybe_definition = LanguageServerData::FindDefinition(id, pos)) { 287 SourcePosition definition = *maybe_definition; 288 response.result().SetTo(definition); 289 } else { 290 response.SetNull("result"); 291 } 292 293 writer(std::move(response.GetJsonValue())); 294} 295 296void HandleChangeWatchedFilesNotification( 297 DidChangeWatchedFilesNotification notification, MessageWriter writer) { 298 // TODO(szuend): Implement updates to the TorqueFile list when create/delete 299 // notifications are received. Currently we simply re-compile. 300 RecompileTorqueWithDiagnostics(writer); 301} 302 303void HandleDocumentSymbolRequest(DocumentSymbolRequest request, 304 MessageWriter writer) { 305 DocumentSymbolResponse response; 306 response.set_id(request.id()); 307 308 SourceId id = 309 SourceFileMap::GetSourceId(request.params().textDocument().uri()); 310 311 for (const auto& symbol : LanguageServerData::SymbolsForSourceId(id)) { 312 DCHECK(symbol->IsUserDefined()); 313 if (symbol->IsMacro()) { 314 Macro* macro = Macro::cast(symbol); 315 SymbolInformation info = response.add_result(); 316 info.set_name(macro->ReadableName()); 317 info.set_kind(SymbolKind::kFunction); 318 info.location().SetTo(macro->Position()); 319 } else if (symbol->IsBuiltin()) { 320 Builtin* builtin = Builtin::cast(symbol); 321 SymbolInformation info = response.add_result(); 322 info.set_name(builtin->ReadableName()); 323 info.set_kind(SymbolKind::kFunction); 324 info.location().SetTo(builtin->Position()); 325 } else if (symbol->IsGenericCallable()) { 326 GenericCallable* generic = GenericCallable::cast(symbol); 327 SymbolInformation info = response.add_result(); 328 info.set_name(generic->name()); 329 info.set_kind(SymbolKind::kFunction); 330 info.location().SetTo(generic->Position()); 331 } else if (symbol->IsTypeAlias()) { 332 const Type* type = TypeAlias::cast(symbol)->type(); 333 SymbolKind kind = 334 type->IsClassType() ? SymbolKind::kClass : SymbolKind::kStruct; 335 336 SymbolInformation sym = response.add_result(); 337 sym.set_name(type->ToString()); 338 sym.set_kind(kind); 339 sym.location().SetTo(symbol->Position()); 340 } 341 } 342 343 // Trigger empty array creation in case no symbols were found. 344 USE(response.result_size()); 345 346 writer(std::move(response.GetJsonValue())); 347} 348 349} // namespace 350 351void HandleMessage(JsonValue raw_message, MessageWriter writer) { 352 Request<bool> request(std::move(raw_message)); 353 354 // We ignore responses for now. They are matched to requests 355 // by id and don't have a method set. 356 // TODO(szuend): Implement proper response handling for requests 357 // that originate from the server. 358 if (!request.has_method()) { 359 Logger::Log("[info] Unhandled response with id ", request.id(), "\n\n"); 360 return; 361 } 362 363 const std::string method = request.method(); 364 if (method == "initialize") { 365 HandleInitializeRequest( 366 InitializeRequest(std::move(request.GetJsonValue())), writer); 367 } else if (method == "initialized") { 368 HandleInitializedNotification(writer); 369 } else if (method == "torque/fileList") { 370 HandleTorqueFileListNotification( 371 TorqueFileListNotification(std::move(request.GetJsonValue())), writer); 372 } else if (method == "textDocument/definition") { 373 HandleGotoDefinitionRequest( 374 GotoDefinitionRequest(std::move(request.GetJsonValue())), writer); 375 } else if (method == "workspace/didChangeWatchedFiles") { 376 HandleChangeWatchedFilesNotification( 377 DidChangeWatchedFilesNotification(std::move(request.GetJsonValue())), 378 writer); 379 } else if (method == "textDocument/documentSymbol") { 380 HandleDocumentSymbolRequest( 381 DocumentSymbolRequest(std::move(request.GetJsonValue())), writer); 382 } else { 383 Logger::Log("[error] Message of type ", method, " is not handled!\n\n"); 384 } 385} 386 387} // namespace ls 388} // namespace torque 389} // namespace internal 390} // namespace v8 391