| // Copyright 2021 The Verible Authors. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| // | |
| #include "verilog/tools/ls/verible-lsp-adapter.h" | |
| #include "common/lsp/lsp-protocol-operators.h" | |
| #include "common/lsp/lsp-protocol.h" | |
| #include "common/text/text_structure.h" | |
| #include "nlohmann/json.hpp" | |
| #include "verilog/analysis/verilog_analyzer.h" | |
| #include "verilog/analysis/verilog_linter.h" | |
| #include "verilog/formatting/format_style_init.h" | |
| #include "verilog/formatting/formatter.h" | |
| #include "verilog/parser/verilog_token_enum.h" | |
| #include "verilog/tools/ls/document-symbol-filler.h" | |
| #include "verilog/tools/ls/lsp-parse-buffer.h" | |
| namespace verilog { | |
| // Convert our representation of a linter violation to a LSP-Diagnostic | |
| static verible::lsp::Diagnostic ViolationToDiagnostic( | |
| const verible::LintViolationWithStatus &v, | |
| const verible::TextStructureView &text) { | |
| const verible::LintViolation &violation = *v.violation; | |
| const verible::LineColumnRange range = text.GetRangeForToken(violation.token); | |
| const char *fix_msg = violation.autofixes.empty() ? "" : " (fix available)"; | |
| return verible::lsp::Diagnostic{ | |
| .range = | |
| { | |
| .start = {.line = range.start.line, | |
| .character = range.start.column}, | |
| .end = {.line = range.end.line, .character = range.end.column}, | |
| }, | |
| .message = absl::StrCat(violation.reason, " ", v.status->url, "[", | |
| v.status->lint_rule_name, "]", fix_msg), | |
| }; | |
| } | |
| std::vector<verible::lsp::Diagnostic> CreateDiagnostics( | |
| const BufferTracker &tracker, int message_limit) { | |
| // Diagnostics should come from the latest state, including all the | |
| // syntax errors. | |
| const ParsedBuffer *const current = tracker.current(); | |
| if (!current) return {}; | |
| const auto &rejected_tokens = current->parser().GetRejectedTokens(); | |
| auto const &lint_violations = | |
| verilog::GetSortedViolations(current->lint_result()); | |
| std::vector<verible::lsp::Diagnostic> result; | |
| int remaining = rejected_tokens.size() + lint_violations.size(); | |
| // TODO: files that generate a lot of messages will create a huge | |
| // output. So we limit the messages here if "message_limit" is set. | |
| // | |
| // We might consider emitting them around the last known | |
| // edit point in the document as this is what the user sees (if we get | |
| // individual edits, not full files pushed). | |
| // | |
| // TODO(hzeller): to limit repetition, maybe limit the number of messages | |
| // coming from the _same_ source if we have a "message_limit". So for | |
| // instance, don't complain on every single line not to use tabs as | |
| // indentation. | |
| if (message_limit >= 0 && remaining > message_limit) { | |
| remaining = message_limit; | |
| } | |
| result.reserve(remaining); | |
| for (const auto &rejected_token : rejected_tokens) { | |
| if (remaining-- <= 0) break; | |
| current->parser().ExtractLinterTokenErrorDetail( | |
| rejected_token, | |
| [&result, &rejected_token]( | |
| const std::string &filename, verible::LineColumnRange range, | |
| verible::ErrorSeverity severity, verible::AnalysisPhase phase, | |
| absl::string_view token_text, absl::string_view context_line, | |
| const std::string &msg) { | |
| std::string message(AnalysisPhaseName(phase)); | |
| absl::StrAppend(&message, " ", ErrorSeverityDescription(severity)); | |
| if (rejected_token.token_info.isEOF()) { | |
| absl::StrAppend(&message, " (unexpected EOF)"); | |
| } else { | |
| absl::StrAppend(&message, " at \"", token_text, "\""); | |
| } | |
| if (!msg.empty()) { // Note: msg is often empty and not useful. | |
| absl::StrAppend(&message, " ", msg); | |
| } | |
| // TODO(hzeller): Add severity into lsp::Diagnostic json. | |
| result.emplace_back(verible::lsp::Diagnostic{ | |
| .range{.start{.line = range.start.line, | |
| .character = range.start.column}, | |
| .end{.line = range.end.line, // | |
| .character = range.end.column}}, | |
| .message = message, | |
| }); | |
| }); | |
| } | |
| for (const auto &v : lint_violations) { | |
| if (remaining-- <= 0) break; | |
| result.emplace_back(ViolationToDiagnostic(v, current->parser().Data())); | |
| } | |
| return result; | |
| } | |
| verible::lsp::FullDocumentDiagnosticReport GenerateDiagnosticReport( | |
| const BufferTracker *tracker, | |
| const verible::lsp::DocumentDiagnosticParams &p) { | |
| verible::lsp::FullDocumentDiagnosticReport result; | |
| if (!tracker) return result; | |
| result.items = CreateDiagnostics(*tracker, -1); // no limit in diagnostic msg | |
| return result; | |
| } | |
| static std::vector<verible::lsp::TextEdit> AutofixToTextEdits( | |
| const verible::AutoFix &fix, const verible::TextStructureView &text) { | |
| std::vector<verible::lsp::TextEdit> result; | |
| // TODO(hzeller): figure out if edits are stacking or are all based | |
| // on the same start status. | |
| const absl::string_view base = text.Contents(); | |
| for (const verible::ReplacementEdit &edit : fix.Edits()) { | |
| verible::LineColumn start = | |
| text.GetLineColAtOffset(edit.fragment.begin() - base.begin()); | |
| verible::LineColumn end = | |
| text.GetLineColAtOffset(edit.fragment.end() - base.begin()); | |
| result.emplace_back(verible::lsp::TextEdit{ | |
| .range = | |
| { | |
| .start = {.line = start.line, .character = start.column}, | |
| .end = {.line = end.line, .character = end.column}, | |
| }, | |
| .newText = edit.replacement, | |
| }); | |
| } | |
| return result; | |
| } | |
| std::vector<verible::lsp::CodeAction> GenerateLinterCodeActions( | |
| const BufferTracker *tracker, const verible::lsp::CodeActionParams &p) { | |
| std::vector<verible::lsp::CodeAction> result; | |
| if (!tracker) return result; | |
| const ParsedBuffer *const current = tracker->current(); | |
| if (!current) return result; | |
| auto const &lint_violations = | |
| verilog::GetSortedViolations(current->lint_result()); | |
| if (lint_violations.empty()) return result; | |
| const verible::TextStructureView &text = current->parser().Data(); | |
| for (const auto &v : lint_violations) { | |
| const verible::LintViolation &violation = *v.violation; | |
| if (violation.autofixes.empty()) continue; | |
| auto diagnostic = ViolationToDiagnostic(v, text); | |
| // The editor usually has the cursor on a line or word, so we | |
| // only want to output edits that are relevant. | |
| if (!rangeOverlap(diagnostic.range, p.range)) continue; | |
| bool preferred_fix = true; | |
| for (const auto &fix : violation.autofixes) { | |
| result.emplace_back(verible::lsp::CodeAction{ | |
| .title = fix.Description(), | |
| .kind = "quickfix", | |
| .diagnostics = {diagnostic}, | |
| .isPreferred = preferred_fix, | |
| // The following is translated from json, map uri -> edits. | |
| // We're only sending changes for one document, the current one. | |
| .edit = {.changes = {{p.textDocument.uri, | |
| AutofixToTextEdits(fix, | |
| current->parser().Data())}}}, | |
| }); | |
| preferred_fix = false; // only the first is preferred. | |
| } | |
| } | |
| return result; | |
| } | |
| nlohmann::json CreateDocumentSymbolOutline( | |
| const BufferTracker *tracker, const verible::lsp::DocumentSymbolParams &p, | |
| bool kate_compatible_tags) { | |
| if (!tracker) return nlohmann::json::array(); | |
| // Only if the tree has been fully parsed, it makes sense to create an outline | |
| const ParsedBuffer *const last_good = tracker->last_good(); | |
| if (!last_good) return nlohmann::json::array(); | |
| verible::lsp::DocumentSymbol toplevel; | |
| const auto &text_structure = last_good->parser().Data(); | |
| verilog::DocumentSymbolFiller filler(kate_compatible_tags, text_structure, | |
| &toplevel); | |
| const auto &syntax_tree = text_structure.SyntaxTree(); | |
| syntax_tree->Accept(&filler); | |
| // We cut down one level, not interested in toplevel file: | |
| return toplevel.children; | |
| } | |
| std::vector<verible::lsp::DocumentHighlight> CreateHighlightRanges( | |
| const BufferTracker *tracker, | |
| const verible::lsp::DocumentHighlightParams &p) { | |
| std::vector<verible::lsp::DocumentHighlight> result; | |
| if (!tracker) return result; | |
| const ParsedBuffer *const current = tracker->current(); | |
| if (!current) return result; | |
| const verible::LineColumn cursor{p.position.line, p.position.character}; | |
| const verible::TextStructureView &text = current->parser().Data(); | |
| const verible::TokenInfo cursor_token = text.FindTokenAt(cursor); | |
| if (cursor_token.token_enum() != SymbolIdentifier) return result; | |
| // Find all the symbols with the same name in the buffer. | |
| // Note, this is very simplistic as it does _not_ take scopes into account. | |
| // For that, we'd need the symbol table, but that implementation is not | |
| // complete yet. | |
| for (const verible::TokenInfo &tok : text.TokenStream()) { | |
| if (tok.token_enum() != cursor_token.token_enum()) continue; | |
| if (tok.text() != cursor_token.text()) continue; | |
| const verible::LineColumnRange range = text.GetRangeForToken(tok); | |
| result.push_back(verible::lsp::DocumentHighlight{ | |
| .range = { | |
| .start = {.line = range.start.line, | |
| .character = range.start.column}, | |
| .end = {.line = range.end.line, .character = range.end.column}, | |
| }}); | |
| } | |
| return result; | |
| } | |
| std::vector<verible::lsp::TextEdit> FormatRange( | |
| const BufferTracker *tracker, | |
| const verible::lsp::DocumentFormattingParams &p) { | |
| std::vector<verible::lsp::TextEdit> result; | |
| if (!tracker) return result; | |
| const ParsedBuffer *const current = tracker->current(); | |
| if (!current) return result; // Can only format if we have latest version. | |
| const verible::TextStructureView &text = current->parser().Data(); | |
| verilog::formatter::FormatStyle format_style; | |
| verilog::formatter::InitializeFromFlags(&format_style); | |
| if (p.has_range) { | |
| // If the cursor is at the very beginning of last line, we don't include | |
| // it in the formatting. | |
| const int last_line_include = p.range.end.character > 0 ? 1 : 0; | |
| const verible::Interval<int> format_lines{ | |
| p.range.start.line + 1, // 1 index based | |
| p.range.end.line + 1 + last_line_include}; | |
| std::string formatted_range; | |
| if (!FormatVerilogRange(text, format_style, &formatted_range, format_lines) | |
| .ok()) | |
| return result; | |
| result.push_back(verible::lsp::TextEdit{ | |
| .range = | |
| { | |
| .start = {.line = format_lines.min - 1, .character = 0}, | |
| .end = {.line = format_lines.max - 1, .character = 0}, | |
| }, | |
| .newText = formatted_range}); | |
| } else { | |
| std::string newText; | |
| if (!FormatVerilog(text, current->uri(), format_style, &newText).ok()) | |
| return result; | |
| // Emit a single edit that replaces the full file. One could consider | |
| // patches maybe; also be safe and don't emit anything if text is the same. | |
| result.push_back(verible::lsp::TextEdit{ | |
| .range = | |
| { | |
| .start = {.line = 0, .character = 0}, | |
| .end = {.line = static_cast<int>(text.Lines().size() - 1), | |
| .character = 0}, | |
| }, | |
| .newText = newText}); | |
| } | |
| return result; | |
| } | |
| } // namespace verilog |