handle_text_document.cpp 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. // Part of the Carbon Language project, under the Apache License v2.0 with LLVM
  2. // Exceptions. See /LICENSE for license information.
  3. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
  4. #include <string>
  5. #include <tuple>
  6. #include <vector>
  7. #include "toolchain/language_server/handle.h"
  8. namespace Carbon::LanguageServer {
  9. // Implements `textDocument/didOpen`:
  10. // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didOpen
  11. auto HandleDidOpenTextDocument(
  12. Context& context, const clang::clangd::DidOpenTextDocumentParams& params)
  13. -> void {
  14. llvm::StringRef filename = params.textDocument.uri.file();
  15. if (!filename.ends_with(".carbon")) {
  16. // Ignore non-Carbon files.
  17. return;
  18. }
  19. auto insert_result = context.files().Insert(
  20. filename, [&] { return Context::File(params.textDocument.uri); });
  21. insert_result.value().SetText(context, params.textDocument.version,
  22. params.textDocument.text);
  23. if (!insert_result.is_inserted()) {
  24. CARBON_DIAGNOSTIC(LanguageServerOpenDuplicateFile, Warning,
  25. "duplicate open file request; updating content");
  26. context.file_emitter().Emit(filename, LanguageServerOpenDuplicateFile);
  27. }
  28. }
  29. // Returns the result of moving from `cursor_index` past `line_count` lines.
  30. static auto GetNthLineIndex(llvm::StringRef contents, size_t cursor_index,
  31. size_t line_count) -> size_t {
  32. for ([[maybe_unused]] auto _ : llvm::seq(line_count)) {
  33. const size_t newline_index = contents.find('\n', cursor_index);
  34. CARBON_CHECK(newline_index != std::string::npos,
  35. "Line number greater than number of lines in the file");
  36. cursor_index = newline_index + 1;
  37. }
  38. return cursor_index;
  39. }
  40. // Takes start and end positions and returns a tuple with start and end
  41. // offsets. Positions are based on row and column numbers in the source
  42. // code. We often need to know the offsets when modifying strings, so
  43. // this function helps us calculate the offsets. It assumes that the start
  44. // position comes before the end position.
  45. static auto PositionToIndex(llvm::StringRef contents,
  46. const clang::clangd::Position& start,
  47. const clang::clangd::Position& end)
  48. -> std::tuple<size_t, size_t> {
  49. size_t start_index =
  50. GetNthLineIndex(contents, /*cursor_index=*/0, start.line);
  51. size_t end_index = GetNthLineIndex(contents, /*cursor_index=*/start_index,
  52. end.line - start.line);
  53. start_index += start.character;
  54. end_index += end.character;
  55. CARBON_CHECK(end_index <= contents.size(),
  56. "Position greater than source code size");
  57. return {start_index, end_index};
  58. }
  59. // LSP allows full and incremental document synchronization. It sends the
  60. // entire source code when doing full sync. However, when doing incremental
  61. // sync, it only sends the list of changes to be performed on the source
  62. // code. It is necessary to apply changes in the order in which they are
  63. // received. These changes can have one of 1) full document or 2) start
  64. // position, end position, and the text which replaces original text between
  65. // these start and end positions. If the range property is absent, then we
  66. // do full sync. Otherwise, we calculate start and end offsets and replace
  67. // the contents between them with the text we received in change.text.
  68. static auto ApplyChanges(
  69. std::string& source,
  70. const std::vector<clang::clangd::TextDocumentContentChangeEvent>&
  71. content_changes) -> void {
  72. for (const auto& change : content_changes) {
  73. // If range is not present, then we replace entire text.
  74. if (!change.range) {
  75. source = change.text;
  76. continue;
  77. }
  78. // TODO: Use lexer line number data but avoid re-lexing multiple times.
  79. auto [start_index, end_index] =
  80. PositionToIndex(source, change.range->start, change.range->end);
  81. source.replace(start_index, end_index - start_index, change.text);
  82. }
  83. }
  84. // Implements `textDocument/didChange`:
  85. // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didChange
  86. auto HandleDidChangeTextDocument(
  87. Context& context, const clang::clangd::DidChangeTextDocumentParams& params)
  88. -> void {
  89. llvm::StringRef filename = params.textDocument.uri.file();
  90. if (!filename.ends_with(".carbon")) {
  91. // Ignore non-Carbon files.
  92. return;
  93. }
  94. if (auto* file = context.LookupFile(filename)) {
  95. // We copy the document to a new string, apply changes to the string, and
  96. // set the string as the new text.
  97. std::string source = file->text().str();
  98. ApplyChanges(source, params.contentChanges);
  99. file->SetText(context, params.textDocument.version, source);
  100. }
  101. }
  102. // Implements `textDocument/didClose`:
  103. // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_didClose
  104. auto HandleDidCloseTextDocument(
  105. Context& context, const clang::clangd::DidCloseTextDocumentParams& params)
  106. -> void {
  107. llvm::StringRef filename = params.textDocument.uri.file();
  108. if (!filename.ends_with(".carbon")) {
  109. // Ignore non-Carbon files.
  110. return;
  111. }
  112. if (context.files().Erase(filename)) {
  113. // Clear diagnostics when the document closes. Otherwise, any diagnostics
  114. // will linger.
  115. context.PublishDiagnostics({.uri = params.textDocument.uri});
  116. } else {
  117. CARBON_DIAGNOSTIC(LanguageServerCloseUnknownFile, Warning,
  118. "tried closing unknown file; ignoring request");
  119. context.file_emitter().Emit(filename, LanguageServerCloseUnknownFile);
  120. }
  121. }
  122. } // namespace Carbon::LanguageServer