| // 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 |