From 672290aebd965a85e103c6b922842eb59a29098c Mon Sep 17 00:00:00 2001 From: David Malcolm Date: Thu, 19 Mar 2026 18:09:19 -0400 Subject: [PATCH 36/36] FIXME: improvements to LSP prototype * fixes to C AST * WIP on implementing "textDocument/declaration" * got "textDocument/declaration" working for simple cases * add lsp::pp_element * WIP on more capabilities * simpler way for wait for debugger * implement "textDocument/hover"; add delegate_textDocument_request_to_worker * WIP on "textDocument/documentHighlight" * WIP on "textDocument/codeAction" * show IDs in json-rpc log * fixup to code actions (fixit hints) * WIP on goto definition * WIP on "textDocument/signatureHelp" * disable some of the logging * WIP on completion * WIP on DejaGnu support for LSP testing * add gcc/c/c-lsp.h --- gcc/c/c-decl.cc | 161 ++++ gcc/c/c-lsp.h | 47 + gcc/c/c-parser.cc | 46 +- gcc/diagnostics/changes.cc | 59 ++ gcc/diagnostics/changes.h | 5 + gcc/diagnostics/sarif-sink.cc | 55 +- gcc/diagnostics/source-printing.cc | 4 +- gcc/json.h | 5 + gcc/lsp/frontend-interface.h | 12 +- gcc/lsp/gcc-impl/server.cc | 195 +++- gcc/lsp/gcc-impl/server.h | 10 +- gcc/lsp/gcc-impl/toplev.cc | 20 +- gcc/lsp/gcc-impl/worker-diagnostics.cc | 94 +- gcc/lsp/gcc-impl/worker.cc | 617 +++++++++++- gcc/lsp/gcc-impl/worker.h | 31 + gcc/lsp/json-rpc.cc | 42 +- gcc/lsp/lsp-logging.cc | 29 +- gcc/lsp/lsp-logging.h | 8 + gcc/lsp/lsp-spec.cc | 842 ++++++++++++++++- gcc/lsp/lsp-spec.h | 888 +++++++++++++++++- gcc/testsuite/gcc.dg/lsp/basics.c | 1 + gcc/testsuite/gcc.dg/lsp/basics.py | 27 + gcc/testsuite/gcc.dg/lsp/cli-client.py | 270 ------ gcc/testsuite/gcc.dg/lsp/codeAction-1.c | 14 + gcc/testsuite/gcc.dg/lsp/codeAction-1.py | 35 + gcc/testsuite/gcc.dg/lsp/completion-1.c | 16 + gcc/testsuite/gcc.dg/lsp/declaration-1.c | 10 + gcc/testsuite/gcc.dg/lsp/declaration-1.py | 37 + gcc/testsuite/gcc.dg/lsp/definition-1.c | 10 + gcc/testsuite/gcc.dg/lsp/definition-1.py | 27 + .../gcc.dg/lsp/documentHighlight-1.c | 10 + .../gcc.dg/lsp/documentHighlight-1.py | 58 ++ gcc/testsuite/gcc.dg/lsp/documentSymbol-1.c | 17 + gcc/testsuite/gcc.dg/lsp/documentSymbol-1.py | 65 ++ gcc/testsuite/gcc.dg/lsp/hover-1.c | 10 + gcc/testsuite/gcc.dg/lsp/hover-1.py | 39 + gcc/testsuite/gcc.dg/lsp/lsp.exp | 31 + gcc/testsuite/gcc.dg/lsp/lsp.py | 239 ----- .../gcc.dg/lsp/publishDiagnostics-1.c | 12 + .../gcc.dg/lsp/publishDiagnostics-1.py | 98 ++ gcc/testsuite/gcc.dg/lsp/signatureHelp-1.c | 9 + gcc/testsuite/gcc.dg/lsp/signatureHelp-1.py | 32 + gcc/testsuite/gcc.dg/lsp/test.c | 12 - gcc/testsuite/gcc.dg/lsp/test.py | 28 - gcc/testsuite/gcc.dg/lsp/toy-ide.py | 182 ---- gcc/testsuite/lib/gcc-dg.exp | 1 + gcc/testsuite/lib/lsp.exp | 93 ++ gcc/testsuite/lib/lsp.py | 631 +++++++++++++ gcc/topics/ast-events-recorder.h | 28 +- gcc/topics/ast-events.h | 6 + 50 files changed, 4305 insertions(+), 913 deletions(-) create mode 100644 gcc/c/c-lsp.h create mode 100644 gcc/testsuite/gcc.dg/lsp/basics.c create mode 100644 gcc/testsuite/gcc.dg/lsp/basics.py delete mode 100644 gcc/testsuite/gcc.dg/lsp/cli-client.py create mode 100644 gcc/testsuite/gcc.dg/lsp/codeAction-1.c create mode 100644 gcc/testsuite/gcc.dg/lsp/codeAction-1.py create mode 100644 gcc/testsuite/gcc.dg/lsp/completion-1.c create mode 100644 gcc/testsuite/gcc.dg/lsp/declaration-1.c create mode 100644 gcc/testsuite/gcc.dg/lsp/declaration-1.py create mode 100644 gcc/testsuite/gcc.dg/lsp/definition-1.c create mode 100644 gcc/testsuite/gcc.dg/lsp/definition-1.py create mode 100644 gcc/testsuite/gcc.dg/lsp/documentHighlight-1.c create mode 100644 gcc/testsuite/gcc.dg/lsp/documentHighlight-1.py create mode 100644 gcc/testsuite/gcc.dg/lsp/documentSymbol-1.c create mode 100644 gcc/testsuite/gcc.dg/lsp/documentSymbol-1.py create mode 100644 gcc/testsuite/gcc.dg/lsp/hover-1.c create mode 100644 gcc/testsuite/gcc.dg/lsp/hover-1.py create mode 100644 gcc/testsuite/gcc.dg/lsp/lsp.exp delete mode 100644 gcc/testsuite/gcc.dg/lsp/lsp.py create mode 100644 gcc/testsuite/gcc.dg/lsp/publishDiagnostics-1.c create mode 100644 gcc/testsuite/gcc.dg/lsp/publishDiagnostics-1.py create mode 100644 gcc/testsuite/gcc.dg/lsp/signatureHelp-1.c create mode 100644 gcc/testsuite/gcc.dg/lsp/signatureHelp-1.py delete mode 100644 gcc/testsuite/gcc.dg/lsp/test.c delete mode 100644 gcc/testsuite/gcc.dg/lsp/test.py delete mode 100644 gcc/testsuite/gcc.dg/lsp/toy-ide.py create mode 100644 gcc/testsuite/lib/lsp.exp create mode 100644 gcc/testsuite/lib/lsp.py diff --git a/gcc/c/c-decl.cc b/gcc/c/c-decl.cc index 8c7679c9895..b7b408a90d3 100644 --- a/gcc/c/c-decl.cc +++ b/gcc/c/c-decl.cc @@ -14429,4 +14429,165 @@ c_lsp_frontend_impl::get_symbols () return reversed; } +bool +c_lsp_frontend_impl::get_CompletionList (const std::vector &ancestry, + lsp::spec::CompletionList &out) +{ +#if 0 + for (auto iter : ancestry) + { + } + while (1) {} // FIXME + gcc_unreachable (); // FIXME +#else + lsp::spec::CompletionList list; + { + lsp::spec::CompletionItem item; + item.label = "test_FIXME_1"; + // FIXME: + lsp::spec::CompletionItemKind kind; + kind.m_val = lsp::spec::CompletionItemKind::values::Function; + item.kind.set_optional_value (kind); + list.items.push_back (std::move (item)); + } + { + lsp::spec::CompletionItem item; + item.label = "test_FIXME_2"; + lsp::spec::CompletionItemKind kind; + kind.m_val = lsp::spec::CompletionItemKind::values::Constant; + item.kind.set_optional_value (kind); + list.items.push_back (std::move (item)); + } + out = std::move (list); + return true; +#endif + +} + +// FIXME: is this the best file for this? + +bool +c_lsp_frontend_impl::get_hover_text (tree node, + lsp::spec::MarkupContent &out) +{ + gcc_assert (node); + + out.kind = "plaintext"; + // FIXME: see if markdown works + + switch (TREE_CODE (node)) + { + default: + return false; + case FUNCTION_DECL: + { + auto pp = global_dc->clone_printer (); + + pp_show_color (pp.get ()) = false; + // FIXME: + // gcc_assert (pp_show_color (pp.get ()) == false); + + tree return_type = TREE_TYPE (TREE_TYPE (node)); + pp_printf (pp.get (), "function %qs\n\n", + lang_hooks.decl_printable_name (node, 1)); + pp_printf (pp.get (), "Returns: %T\n\n", return_type); + pp_printf (pp.get (), "Parameters\n"); + for (tree parm = DECL_ARGUMENTS (node); parm; parm = TREE_CHAIN (parm)) + pp_printf (pp.get (), "- %T %s\n", + TREE_TYPE (parm), + lang_hooks.decl_printable_name (parm, 0)); + pp_printf (pp.get (), "\n%T %s(", + return_type, + lang_hooks.decl_printable_name (node, 1)); + for (tree parm = DECL_ARGUMENTS (node); parm; parm = TREE_CHAIN (parm)) + { + if (parm != DECL_ARGUMENTS (node)) + pp_printf (pp.get (), ", "); + pp_printf (pp.get (), "%T %s", + TREE_TYPE (parm), + lang_hooks.decl_printable_name (parm, 0)); + } + pp_printf (pp.get (), ")\n"); + out.value = pp_formatted_text (pp.get ()); + return true; + } + case VAR_DECL: + { + auto pp = global_dc->clone_printer (); + pp_printf (pp.get (), "variable %qs\n\n", + lang_hooks.decl_printable_name (node, 1)); + pp_printf (pp.get (), "Type: %T\n\n", TREE_TYPE (node)); + out.value = pp_formatted_text (pp.get ()); + return true; + } + case PARM_DECL: + { + auto pp = global_dc->clone_printer (); + pp_printf (pp.get (), "parameter %qs of %qE\n\n", + lang_hooks.decl_printable_name (node, 1), + DECL_CONTEXT (node)); + pp_printf (pp.get (), "Type: %T\n\n", TREE_TYPE (node)); + out.value = pp_formatted_text (pp.get ()); + return true; + } + } +} + +// FIXME: is this the best file for this? + +bool +c_lsp_frontend_impl::get_SignatureHelp (tree fndecl, + lsp::spec::SignatureHelp &out) +{ + lsp::spec::SignatureHelp help; + + lsp::spec::SignatureInformation sig_info; + tree fntype = TREE_TYPE (fndecl); + tree return_type = TREE_TYPE (fntype); + auto pp = global_dc->clone_printer (); + + pp_show_color (pp.get ()) = false; + // FIXME: + // gcc_assert (pp_show_color (pp.get ()) == false); + + pp_printf (pp.get (), "\n%T %s(", + return_type, + lang_hooks.decl_printable_name (fndecl, 1)); + for (tree parm = DECL_ARGUMENTS (fndecl); parm; parm = TREE_CHAIN (parm)) + { + if (parm != DECL_ARGUMENTS (fndecl)) + pp_printf (pp.get (), ", "); + pp_printf (pp.get (), "%T %s", + TREE_TYPE (parm), + lang_hooks.decl_printable_name (parm, 0)); + } + pp_printf (pp.get (), ")"); + sig_info.label = pp_formatted_text (pp.get ()); + lsp::vector params; + for (tree parm = DECL_ARGUMENTS (fndecl); parm; parm = TREE_CHAIN (parm)) + { + auto pp = global_dc->clone_printer (); + pp_printf (pp.get (), "%T %s", + TREE_TYPE (parm), + lang_hooks.decl_printable_name (parm, 0)); + lsp::spec::ParameterInformation param_info; + param_info.label = pp_formatted_text (pp.get ()); + params.push_back (std::move (param_info)); + } + sig_info.parameters.set_optional_value (std::move (params)); + + help.signatures.push_back (std::move (sig_info)); + + help.activeSignature.set_optional_value (0); + + /* FIXME: ideally we'd figure out which param we're at + from the ast node. */ + help.activeParameter.set_optional_value (0); + + out = std::move (help); + return true; +} + + + #include "gt-c-c-decl.h" diff --git a/gcc/c/c-lsp.h b/gcc/c/c-lsp.h new file mode 100644 index 00000000000..cfcadef6677 --- /dev/null +++ b/gcc/c/c-lsp.h @@ -0,0 +1,47 @@ +/* FIXME. + Copyright (C) 2026 Free Software Foundation, Inc. + +This file is part of GCC. + +GCC is free software; you can redistribute it and/or modify it under +the terms of the GNU General Public License as published by the Free +Software Foundation; either version 3, or (at your option) any later +version. + +GCC is distributed in the hope that it will be useful, but WITHOUT ANY +WARRANTY; without even the implied warranty of MERCHANTABILITY or +FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +for more details. + +You should have received a copy of the GNU General Public License +along with GCC; see the file COPYING3. If not see +. */ + +#ifndef GCC_C_LSP_H +#define GCC_C_LSP_H + +#include "lsp/frontend-interface.h" + +class c_lsp_frontend_impl : public lsp::frontend_worker_interface +{ +public: + lsp::vector + get_symbols () final override; + + bool + get_CompletionList (const std::vector &ancestry, + lsp::spec::CompletionList &out) final override; + + bool + get_hover_text (tree node, + lsp::spec::MarkupContent &out) final override; + + bool + get_SignatureHelp (tree fndecl, + lsp::spec::SignatureHelp &out) final override; + +private: + lsp::spec::DocumentSymbol make_symbol_for_decl (tree decl); +}; + +#endif /* ! GCC_C_LSP_H */ diff --git a/gcc/c/c-parser.cc b/gcc/c/c-parser.cc index 186666981a8..661474b1a73 100644 --- a/gcc/c/c-parser.cc +++ b/gcc/c/c-parser.cc @@ -321,19 +321,23 @@ public: // FIXME: unlikely // gengtype doesn't understand [[unlikely]] , and is it legal // in c++14 anyway? - if (parser->ast_events_channel) - parser->ast_events_channel->publish + if (m_parser->ast_events_channel) + m_parser->ast_events_channel->publish (gcc::topics::ast_events::begin_node {kind, get_next_loc ()}); } ~auto_notify_ast_node () { // FIXME: unlikely + if (m_parser->ast_events_channel) + m_parser->ast_events_channel->publish + (gcc::topics::ast_events::end_node {m_kind, #if 0 - if (parser->ast_events_channel) - parser->ast_events_channel->publish - (gcc::topics::ast_events::end_node {m_kind, get_prev_loc ()}); + get_prev_loc () +#else + get_next_loc () #endif + }); } location_t @@ -2136,7 +2140,7 @@ private: static void c_parser_translation_unit (c_parser *parser) { - auto_notify_ast_node (parser, "translation-unit"); + auto_notify_ast_node sentinel (parser, "translation-unit"); if (c_parser_next_token_is (parser, CPP_EOF)) { @@ -2238,7 +2242,7 @@ c_parser_translation_unit (c_parser *parser) static void c_parser_external_declaration (c_parser *parser) { - auto_notify_ast_node (parser, "external-declaration"); + auto_notify_ast_node sentinel (parser, "external-declaration"); int ext; switch (c_parser_peek_token (parser)->type) @@ -4183,7 +4187,7 @@ report_bad_enum_name (c_parser *parser) static struct c_typespec c_parser_enum_specifier (c_parser *parser) { - auto_notify_ast_node (parser, "enum-specifier"); + auto_notify_ast_node sentinel (parser, "enum-specifier"); struct c_typespec ret; bool have_std_attrs; @@ -4456,7 +4460,7 @@ c_parser_enum_specifier (c_parser *parser) static struct c_typespec c_parser_struct_or_union_specifier (c_parser *parser) { - auto_notify_ast_node (parser, "struct-or-union-specifier"); + auto_notify_ast_node sentinel (parser, "struct-or-union-specifier"); struct c_typespec ret; bool have_std_attrs; tree std_attrs = NULL_TREE; @@ -5056,7 +5060,7 @@ struct c_declarator * c_parser_declarator (c_parser *parser, bool type_seen_p, c_dtr_syn kind, bool *seen_id) { - auto_notify_ast_node (parser, "declarator"); + auto_notify_ast_node sentinel (parser, "declarator"); /* Parse any initial pointer part. */ if (c_parser_next_token_is (parser, CPP_MULT)) @@ -7227,6 +7231,7 @@ c_parser_initval (c_parser *parser, struct c_expr *after, static tree c_parser_compound_statement (c_parser *parser, location_t *endlocp) { + auto_notify_ast_node sentinel (parser, "compound-statement"); tree stmt; location_t brace_loc; brace_loc = c_parser_peek_token (parser)->location; @@ -8154,6 +8159,7 @@ mangle_metadirective_region_label (c_parser *parser, tree name) static void c_parser_label (c_parser *parser, tree std_attrs) { + auto_notify_ast_node sentinel (parser, "label"); location_t loc1 = c_parser_peek_token (parser)->location; tree label = NULL_TREE; @@ -8984,6 +8990,7 @@ c_parser_if_statement (c_parser *parser, bool *if_p, vec *chain) static void c_parser_switch_statement (c_parser *parser, bool *if_p, tree before_labels) { + auto_notify_ast_node sentinel (parser, "switch-statement"); struct c_expr ce; tree block, expr, body; unsigned char save_in_statement; @@ -9066,6 +9073,7 @@ static void c_parser_while_statement (c_parser *parser, bool ivdep, unsigned short unroll, bool novector, bool *if_p, tree before_labels) { + auto_notify_ast_node sentinel (parser, "while-statement"); tree block, cond, body; unsigned char save_in_statement; location_t loc; @@ -9147,6 +9155,7 @@ static void c_parser_do_statement (c_parser *parser, bool ivdep, unsigned short unroll, bool novector, tree before_labels) { + auto_notify_ast_node sentinel (parser, "do-statement"); tree block, cond, body; unsigned char save_in_statement; location_t loc; @@ -9273,6 +9282,7 @@ static void c_parser_for_statement (c_parser *parser, bool ivdep, unsigned short unroll, bool novector, bool *if_p, tree before_labels) { + auto_notify_ast_node sentinel (parser, "for-statement"); tree block, cond, incr, body; unsigned char save_in_statement; tree save_objc_foreach_break_label, save_objc_foreach_continue_label; @@ -9875,6 +9885,7 @@ c_parser_asm_goto_operands (c_parser *parser) struct c_expr c_parser_string_literal (c_parser *parser, bool translate, bool wide_ok) { + auto_notify_ast_node sentinel (parser, "string-literal"); struct c_expr ret; size_t count; struct obstack str_ob; @@ -10166,6 +10177,7 @@ static struct c_expr c_parser_conditional_expression (c_parser *parser, struct c_expr *after, tree omp_atomic_lhs) { + auto_notify_ast_node sentinel (parser, "conditional-statement"); struct c_expr cond, exp1, exp2, ret; location_t start, cond_loc, colon_loc; bool save_c_omp_array_section_p = c_omp_array_section_p; @@ -10653,6 +10665,7 @@ c_parser_compound_literal_scspecs (c_parser *parser) static struct c_expr c_parser_cast_expression (c_parser *parser, struct c_expr *after) { + auto_notify_ast_node sentinel (parser, "cast-expression"); location_t cast_loc = c_parser_peek_token (parser)->location; gcc_assert (!after || c_dialect_objc ()); if (after) @@ -10756,6 +10769,7 @@ c_parser_cast_expression (c_parser *parser, struct c_expr *after) static struct c_expr c_parser_unary_expression (c_parser *parser) { + auto_notify_ast_node sentinel (parser, "unary-expression"); int ext; struct c_expr ret, op; location_t op_loc = c_parser_peek_token (parser)->location; @@ -11786,6 +11800,14 @@ get_counted_by_ref (tree ref) return NULL_TREE; } +static void +maybe_set_tree_for_ast_node (c_parser *parser, tree expr) +{ + if (parser->ast_events_channel) + parser->ast_events_channel->publish + (gcc::topics::ast_events::set_tree ({expr})); +} + /* Parse a postfix expression (C90 6.3.1-6.3.2, C99 6.5.1-6.5.2, C11 6.5.1-6.5.2). Compound literals aren't handled here; callers have to call c_parser_postfix_expression_after_paren_type on encountering them. @@ -11854,6 +11876,7 @@ get_counted_by_ref (tree ref) static struct c_expr c_parser_postfix_expression (c_parser *parser) { + auto_notify_ast_node sentinel (parser, "postfix-expression"); struct c_expr expr, e1; struct c_type_name *t1, *t2; location_t loc = c_parser_peek_token (parser)->location; @@ -11908,12 +11931,15 @@ c_parser_postfix_expression (c_parser *parser) { case C_ID_ID: { + auto_notify_ast_node sentinel (parser, "identifier"); tree id = c_parser_peek_token (parser)->value; c_parser_consume_token (parser); expr.value = build_external_ref (loc, id, (c_parser_peek_token (parser)->type == CPP_OPEN_PAREN), &expr.original_type); + + maybe_set_tree_for_ast_node (parser, expr.value); set_c_expr_source_range (&expr, tok_range); break; } diff --git a/gcc/diagnostics/changes.cc b/gcc/diagnostics/changes.cc index a416e4b4c59..3badfb45bba 100644 --- a/gcc/diagnostics/changes.cc +++ b/gcc/diagnostics/changes.cc @@ -25,6 +25,7 @@ along with GCC; see the file COPYING3. If not see #include "pretty-print.h" #include "diagnostics/color.h" #include "diagnostics/file-cache.h" +#include "intl.h" #include "selftest.h" namespace diagnostics { @@ -899,6 +900,64 @@ changed_line::ensure_terminated () m_content[m_len] = '\0'; } +bool +try_to_print_fixit_hint_description (pretty_printer *pp, + const fixit_hint &hint, + file_cache &fc) +{ + if (hint.insertion_p ()) + { + pp_printf (pp, G_("Insert %qs"), hint.get_string ()); + return true; + } + else + { + /* Try to get prior content. */ + expanded_location start = expand_location (hint.get_start_loc ()); + expanded_location next_loc = expand_location (hint.get_next_loc ()); + if (start.file != next_loc.file) + return false; + if (start.line != next_loc.line) + return false; + if (start.column == 0) + return false; + if (next_loc.column == 0) + return false; + + const int start_offset = start.column - 1; + const int next_offset = next_loc.column - 1; + if (next_offset <= start_offset) + return false; + + size_t victim_len = next_offset - start_offset; + + char_span existing_line = fc.get_source_line (start.file, start.line); + if (!existing_line) + return false; + + label_text existing_text + = label_text::take (existing_line.subspan (start_offset, + victim_len).xstrdup ()); + + if (hint.deletion_p ()) + { + // Removal + pp_printf (pp, G_("Delete %qs"), + existing_text.get ()); + return true; + } + else + { + // Replacement + gcc_assert (hint.replacement_p ()); + pp_printf (pp, G_("Replace %qs with %qs"), + existing_text.get (), + hint.get_string ()); + return true; + } + } +} + #if CHECKING_P /* Selftests of code-editing. */ diff --git a/gcc/diagnostics/changes.h b/gcc/diagnostics/changes.h index 9d5a1460275..50b5f40f665 100644 --- a/gcc/diagnostics/changes.h +++ b/gcc/diagnostics/changes.h @@ -72,6 +72,11 @@ class change_set typed_splay_tree m_files; }; +extern bool +try_to_print_fixit_hint_description (pretty_printer *pp, + const fixit_hint &hint, + file_cache &fc); + } // namespace diagnostics::changes } // namespace diagnostics diff --git a/gcc/diagnostics/sarif-sink.cc b/gcc/diagnostics/sarif-sink.cc index f601dbe7c5f..60fd31f4851 100644 --- a/gcc/diagnostics/sarif-sink.cc +++ b/gcc/diagnostics/sarif-sink.cc @@ -50,6 +50,7 @@ struct sockaddr_un { #include "diagnostics/buffering.h" #include "diagnostics/dumping.h" #include "diagnostics/logging.h" +#include "diagnostics/changes.h" #include "json.h" #include "cpplib.h" #include "diagnostics/logical-locations.h" @@ -70,7 +71,6 @@ struct sockaddr_un { #include "demangle.h" #include "backtrace.h" #include "xml.h" -#include "intl.h" namespace diagnostics { @@ -3810,55 +3810,12 @@ sarif_builder:: make_message_describing_fix_it_hint (const fixit_hint &hint) const { pretty_printer pp; - if (hint.insertion_p ()) - pp_printf (&pp, G_("Insert %qs"), hint.get_string ()); + if (changes::try_to_print_fixit_hint_description + (&pp, hint, + get_context ().get_file_cache ())) + return make_message_object (pp_formatted_text (&pp)); else - { - /* Try to get prior content. */ - expanded_location start = expand_location (hint.get_start_loc ()); - expanded_location next_loc = expand_location (hint.get_next_loc ()); - if (start.file != next_loc.file) - return nullptr; - if (start.line != next_loc.line) - return nullptr; - if (start.column == 0) - return nullptr; - if (next_loc.column == 0) - return nullptr; - - const int start_offset = start.column - 1; - const int next_offset = next_loc.column - 1; - if (next_offset <= start_offset) - return nullptr; - - size_t victim_len = next_offset - start_offset; - - char_span existing_line = get_context () - .get_file_cache () - .get_source_line (start.file, start.line); - if (!existing_line) - return nullptr; - - label_text existing_text - = label_text::take (existing_line.subspan (start_offset, - victim_len).xstrdup ()); - - if (hint.deletion_p ()) - { - // Removal - pp_printf (&pp, G_("Delete %qs"), - existing_text.get ()); - } - else - { - // Replacement - gcc_assert (hint.replacement_p ()); - pp_printf (&pp, G_("Replace %qs with %qs"), - existing_text.get (), - hint.get_string ()); - } - } - return make_message_object (pp_formatted_text (&pp)); + return nullptr; } /* Make a "fix" object (SARIF v2.1.0 section 3.55) for RICHLOC. */ diff --git a/gcc/diagnostics/source-printing.cc b/gcc/diagnostics/source-printing.cc index db9b3c1f241..66c47413bea 100644 --- a/gcc/diagnostics/source-printing.cc +++ b/gcc/diagnostics/source-printing.cc @@ -1094,7 +1094,7 @@ layout_range::layout_range (const exploc_with_display_col &start_exploc, start: (col=22, line=2) finish: (col=38, line=2) - |00000011111111112222222222333333333344444444444 + |00000001111111111222222222233333333334444444444 |34567890123456789012345678901234567890123456789 --+----------------------------------------------- 01|bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb @@ -1105,7 +1105,7 @@ layout_range::layout_range (const exploc_with_display_col &start_exploc, start: (col=14, line=3) finish: (col=08, line=5) - |00000011111111112222222222333333333344444444444 + |00000001111111111222222222233333333334444444444 |34567890123456789012345678901234567890123456789 --+----------------------------------------------- 01|bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb diff --git a/gcc/json.h b/gcc/json.h index 54bc00e964e..c3353e9d7b1 100644 --- a/gcc/json.h +++ b/gcc/json.h @@ -382,6 +382,11 @@ class literal : public value enum kind m_kind; }; +inline std::unique_ptr +make_null () +{ + return std::make_unique (JSON_NULL); +} template inline bool diff --git a/gcc/lsp/frontend-interface.h b/gcc/lsp/frontend-interface.h index 94b95c4167c..b78bdfd030c 100644 --- a/gcc/lsp/frontend-interface.h +++ b/gcc/lsp/frontend-interface.h @@ -39,7 +39,17 @@ public: virtual lsp::vector get_symbols () = 0; -private: + virtual bool + get_CompletionList (const std::vector &ancestry, + spec::CompletionList &out) = 0; + + virtual bool + get_hover_text (tree node, + spec::MarkupContent &out) = 0; + + virtual bool + get_SignatureHelp (tree fndecl, + spec::SignatureHelp &out) = 0; }; } // namespace lsp diff --git a/gcc/lsp/gcc-impl/server.cc b/gcc/lsp/gcc-impl/server.cc index 77511e200d0..17f610267ca 100644 --- a/gcc/lsp/gcc-impl/server.cc +++ b/gcc/lsp/gcc-impl/server.cc @@ -146,8 +146,10 @@ void lsp::gcc_impl::server::json_from_worker:: on_malformed_json (json::error &&) { - LSP_LOG_SCOPE (get_logger ()); - gcc_unreachable (); + auto logger = get_logger (); + LSP_LOG_SCOPE (logger); + if (logger) + logger->log ("got malformed json from worker: FIXME"); } void @@ -239,11 +241,11 @@ start_worker_subprocess () // FIXME: args.push_back ("/proc/self/exe"); // FIXME: Linux-specific ? args.push_back ("-quiet"); - //args.push_back ("/home/david/nomad-coding/gcc-newgit-no-function-points/src/gcc/testsuite/gcc.dg/lsp/test.c"); args.push_back ("-Wall"); // FIXME args.push_back ("-flsp-worker"); if (flag_lsp_worker_wait_for_debugger) args.push_back ("-flsp-worker-wait-for-debugger"); + // FIXME: pass on worker logging options auto launch_result = subprocess::launch (std::move (args), get_logger ()); launch_result.set_debug_name @@ -361,7 +363,8 @@ lsp::gcc_impl::server::mediator::mediator (lsp::logger *logger) LSP_LOG_SCOPE (logger); set_logger (logger); - m_client_ipc_connection.set_logger (logger); + if (1) + m_client_ipc_connection.set_logger (logger); m_client_ipc_connection.m_outgoing_sender.set_debug_name ("stdout from server for client"); @@ -374,17 +377,8 @@ lsp::gcc_impl::server::mediator::mediator (lsp::logger *logger) m_octets_from_client.set_json_rpc_sink (&m_json_from_client); m_octets_from_client.set_logger (logger); - m_json_from_client.set_logger (logger); - - -#if 0 - m_plumbing.connect (&m_json_rpc_to_lsp_server_for_client, - logger); - m_json_rpc_to_lsp_server_for_client.set_logger (logger); - m_json_rpc_to_lsp_server_for_client.set_lsp_server - (&m_impl_server_for_client); - m_impl_server_for_client.set_logger (logger); -#endif + if (1) + m_json_from_client.set_logger (logger); } void @@ -396,7 +390,8 @@ lsp::gcc_impl::server::mediator::handle_events_forever () while (1) { ipc::event_poll evp; - evp.set_logger (logger); + if (1) + evp.set_logger (logger); evp.add_endpoint (&m_client_ipc_connection.m_incoming_receiver); evp.add_endpoint (&m_client_ipc_connection.m_outgoing_sender); @@ -428,14 +423,12 @@ consume_client_request (json_rpc::request &&req) auto unmarshal_result = spec::InitializeParams::from_json (req.get_id (), *req.get_params ()); - if (unmarshal_result.get_error ()) + if (auto err = unmarshal_result.get_error ()) { - gcc_unreachable (); // FIXME -#if 0 send_response_to_client - (json_rpc::response::invalid_params (req, - std::move (unmarshal_result))); -#endif + (json_rpc::response::failure + (req.take_id (), + std::move (*unmarshal_result.take_error ()))); return; } @@ -448,11 +441,11 @@ consume_client_request (json_rpc::request &&req) gcc_unreachable (); // return dispatch_call_shutdown (std::move (req)); } - else if (0 == strcmp (method, "textDocument/documentSymbol")) + else if (0 == strcmp (method, "textDocument/codeAction")) { auto unmarshal_result - = spec::DocumentSymbolParams::from_json (req.get_id (), - *req.get_params ()); + = spec::CodeActionParams::from_json (req.get_id (), + *req.get_params ()); if (unmarshal_result.get_error ()) { gcc_unreachable (); // FIXME @@ -464,17 +457,52 @@ consume_client_request (json_rpc::request &&req) return; } - on_client_request_textDocument_documentSymbol - (req.take_id (), - unmarshal_result.take_value ()); - /* We will expect a response from the worker, and forward - that to the client. */ + on_client_request_textDocument_codeAction (req.take_id (), + unmarshal_result.take_value ()); + return; + } + else if (0 == strcmp (method, "textDocument/completion")) + { + delegate_textDocument_request_to_worker + (std::move (req)); + return; + } + else if (0 == strcmp (method, "textDocument/declaration")) + { + delegate_textDocument_request_to_worker + (std::move (req)); return; } -#if 0 else if (0 == strcmp (method, "textDocument/definition")) - return dispatch_textDocument_definition (std::move (req)); -#endif + { + delegate_textDocument_request_to_worker + (std::move (req)); + return; + } + else if (0 == strcmp (method, "textDocument/documentSymbol")) + { + delegate_textDocument_request_to_worker + (std::move (req)); + return; + } + else if (0 == strcmp (method, "textDocument/documentHighlight")) + { + delegate_textDocument_request_to_worker + (std::move (req)); + return; + } + else if (0 == strcmp (method, "textDocument/hover")) + { + delegate_textDocument_request_to_worker + (std::move (req)); + return; + } + else if (0 == strcmp (method, "textDocument/signatureHelp")) + { + delegate_textDocument_request_to_worker + (std::move (req)); + return; + } send_response_to_client (json_rpc::response::method_not_found (std::move (req))); @@ -565,20 +593,67 @@ on_client_request_initialize (std::unique_ptr req_id, void lsp::gcc_impl::server::mediator:: -on_client_request_textDocument_documentSymbol (std::unique_ptr req_id, - spec::DocumentSymbolParams &&p) +on_client_request_textDocument_codeAction (std::unique_ptr req_id, + spec::CodeActionParams &&p) { LSP_LOG_SCOPE (get_logger ()); - auto iter = m_open_documents.find (p.textDocument.uri); + vector result; + + // assume we stashed the fix-it information in the "data" for each diagnostic + for (auto diag : p.context.diagnostics) + if (auto v = diag.data.get_optional_value ()) + { + lsp::unmarshaller u (req_id.get ()); + auto action_res = lsp::spec::CodeAction::from_json (u, *v->m_val); + if (action_res.get_error ()) + { + // FIXME + gcc_unreachable (); // FIXME + } + result.push_back (action_res.take_value ()); + } + + send_response_to_client + (json_rpc::response::success (std::move (req_id), result.to_json ())); +} + +/* FIXME. */ + +template +void +lsp::gcc_impl::server::mediator:: +delegate_textDocument_request_to_worker (json_rpc::request &&req) +{ + LSP_LOG_SCOPE (get_logger ()); + + auto unmarshal_result + = RequestParams::from_json (req.get_id (), + *req.get_params ()); + if (unmarshal_result.get_error ()) + { + gcc_unreachable (); // FIXME +#if 0 + send_response_to_client + (json_rpc::response::invalid_params (req, + std::move (unmarshal_result))); +#endif + return; + } + + auto params = unmarshal_result.take_value (); + + auto iter = m_open_documents.find (params.textDocument.uri); gcc_assert (iter != m_open_documents.end ()); auto doc_ver = iter->second->get_most_recent_version (); gcc_assert (doc_ver); doc_ver->get_worker_subprocess ()->send_request - (json_rpc::request ("textDocument/documentSymbol", - p.to_json (), - std::move (req_id))); + (json_rpc::request (req.get_method (), + params.to_json (), + req.take_id ())); + /* We will expect a response from the worker, and forward + that to the client. */ } // Handling of notifications from client @@ -665,15 +740,55 @@ record_client_init_params (lsp::spec::InitializeParams &&p) = std::make_unique (std::move (p)); } +lsp::spec::SignatureHelpOptions +lsp::gcc_impl::server::mediator::get_SignatureHelpOptions () +{ + lsp::spec::SignatureHelpOptions opts; + lsp::vector trigger_chars; + trigger_chars.push_back ("("); + trigger_chars.push_back (","); + opts.triggerCharacters.set_optional_value (std::move (trigger_chars)); + return opts; +} + lsp::spec::ServerCapabilities lsp::gcc_impl::server::mediator::get_ServerCapabilities () { lsp::spec::ServerCapabilities result; + #if 1 + result.completionProvider.set_optional_value + (lsp::spec::CompletionOptions ()); // FIXME +#endif + + result.hoverProvider.set_optional_value + (lsp::spec::HoverOptions ()); + + result.signatureHelpProvider.set_optional_value + (get_SignatureHelpOptions ()); + + result.declarationProvider.set_optional_value + (lsp::spec::boolean (true)); + + result.definitionProvider.set_optional_value + (lsp::spec::boolean (true)); + + result.documentHighlightProvider.set_optional_value + (lsp::spec::boolean (true)); + result.documentSymbolProvider.set_optional_value - (lsp::spec::boolean (true)); // FIXME + (lsp::spec::boolean (true)); + + result.codeActionProvider.set_optional_value + (lsp::spec::boolean (true)); + +#if 0 + result.semanticTokensProvider.set_optional_value + (lsp::spec::SemanticTokensOptions ()); // FIXME #endif + // FIXME + return result; } diff --git a/gcc/lsp/gcc-impl/server.h b/gcc/lsp/gcc-impl/server.h index ff919797acf..1b0b5adc327 100644 --- a/gcc/lsp/gcc-impl/server.h +++ b/gcc/lsp/gcc-impl/server.h @@ -244,8 +244,12 @@ public: spec::InitializeParams &&p); void - on_client_request_textDocument_documentSymbol (std::unique_ptr req_id, - spec::DocumentSymbolParams &&p); + on_client_request_textDocument_codeAction (std::unique_ptr req_id, + spec::CodeActionParams &&p); + + template + void + delegate_textDocument_request_to_worker (json_rpc::request &&req); // Handling of notifications from client @@ -288,6 +292,8 @@ public: spec::InitializeResult::serverInfo_t get_serverInfo (); private: + spec::SignatureHelpOptions get_SignatureHelpOptions (); + ipc::connection m_client_ipc_connection; octets_to_json_rpc m_octets_from_client; json_from_client m_json_from_client; diff --git a/gcc/lsp/gcc-impl/toplev.cc b/gcc/lsp/gcc-impl/toplev.cc index 87b413e064f..0b737d39227 100644 --- a/gcc/lsp/gcc-impl/toplev.cc +++ b/gcc/lsp/gcc-impl/toplev.cc @@ -44,7 +44,12 @@ static lsp::logger *lsp_logger; lsp::logger * make_logger (std::string prefix, const char *color_name) { - bool show_color = true; // FIXME: only if at a tty, or respect options +#if 0 + // FIXME: + return nullptr; +#endif + + bool show_color = false; // FIXME: only if at a tty, or respect options const char *start_color = colorize_start (show_color, color_name, strlen (color_name)); const char *stop_color = colorize_stop (show_color); @@ -70,16 +75,19 @@ do_init (bool is_server, bool wait_for_debugger) if (wait_for_debugger) { - lsp_logger->log ("waiting for debugger; use"); + lsp_logger->log ("waiting for debugger due to %s; use", + (is_server + ? "-flsp-server-wait-for-debugger" + : "-flsp-worker-wait-for-debugger")); lsp_logger->emit_start_color (); - fprintf (stderr, " $ gdb attach -p %i\n", getpid ()); - fprintf (stderr, " (gdb) set wait_for_debugger = 0\n"); + fprintf (stderr, " (gdb) attach %i\n", getpid ()); fprintf (stderr, " (gdb) continue\n"); lsp_logger->emit_stop_color (); - while (wait_for_debugger) + if (raise (SIGSTOP) != 0) { - // wait until flag is cleared in debugger + lsp_logger->log ("%qs failed; exiting", "raise"); + exit (EXIT_FAILURE); } } } diff --git a/gcc/lsp/gcc-impl/worker-diagnostics.cc b/gcc/lsp/gcc-impl/worker-diagnostics.cc index d041e087545..32ae9f0e698 100644 --- a/gcc/lsp/gcc-impl/worker-diagnostics.cc +++ b/gcc/lsp/gcc-impl/worker-diagnostics.cc @@ -34,6 +34,7 @@ along with GCC; see the file COPYING3. If not see #include "diagnostic.h" #include "diagnostics/sink.h" #include "diagnostics/dumping.h" +#include "diagnostics/changes.h" #include "uris.h" using top_level_diagnostic @@ -145,7 +146,8 @@ lsp::gcc_impl::worker::diagnostics::sink::on_end_group () { if (m_mediator.done_compiling_p ()) { - gcc_unreachable (); // FIXME + // FIXME: + // gcc_unreachable (); // FIXME } else { @@ -297,12 +299,12 @@ get_severity_for_kind (diagnostics::kind kind) { case diagnostics::kind::debug: case diagnostics::kind::note: - result.m_val = lsp::spec::DiagnosticSeverity::Information; + result.m_val = lsp::spec::DiagnosticSeverity::values::Information; break; case diagnostics::kind::anachronism: case diagnostics::kind::warning: - result.m_val = lsp::spec::DiagnosticSeverity::Warning; + result.m_val = lsp::spec::DiagnosticSeverity::values::Warning; break; case diagnostics::kind::error: @@ -310,7 +312,7 @@ get_severity_for_kind (diagnostics::kind kind) case diagnostics::kind::ice: case diagnostics::kind::ice_nobt: case diagnostics::kind::fatal: - result.m_val = lsp::spec::DiagnosticSeverity::Error; + result.m_val = lsp::spec::DiagnosticSeverity::values::Error; break; default: @@ -359,6 +361,75 @@ get_relatedInformation_from_rich_location (const rich_location &rich_loc) return result; } +static lsp::spec::TextEdit +get_TextEdit_from_fixit_hint (const fixit_hint &hint) +{ + lsp::spec::TextEdit result; + /* Both fixit_hint and Range are half-open. */ + result.range.start + = lsp::spec::Position::from_location_t (hint.get_start_loc ()); + result.range.end + = lsp::spec::Position::from_location_t (hint.get_next_loc ()); + result.newText = std::string (hint.get_string (), hint.get_length ()); + return result; +} + +static lsp::spec::WorkspaceEdit +get_WorkspaceEdit_from_from_rich_location (const rich_location &rich_loc) +{ + lsp::spec::map_DocumentUri_to_vector_TextEdit changes; + for (int i = 0; i < rich_loc.get_num_fixit_hints (); ++i) + { + const fixit_hint *hint = rich_loc.get_fixit_hint (i); + + auto text_edit = get_TextEdit_from_fixit_hint (*hint); + + uris::file_uri file_uri (LOCATION_FILE (hint->get_start_loc ())); + lsp::spec::DocumentUri uri (file_uri.to_string ()); + + auto iter = changes.find (uri); + if (iter == changes.end ()) + { + lsp::vector edits; + edits.push_back (std::move (text_edit)); + changes.insert ({uri, std::move (edits)}); + } + else + iter->second.push_back (std::move (text_edit)); + } + lsp::spec::WorkspaceEdit edit; + edit.changes = std::move (changes); + return edit; +} + +/* FIXME. */ + +static bool +maybe_get_CodeAction_from_from_rich_location (const rich_location &rich_loc, + diagnostics::file_cache &fc, + lsp::spec::CodeAction &out) +{ + if (!rich_loc.get_num_fixit_hints ()) + return false; + + out.title = "Fix"; + if (rich_loc.get_num_fixit_hints () == 1) + { + pretty_printer pp; + if (diagnostics::changes::try_to_print_fixit_hint_description + (&pp, + *rich_loc.get_fixit_hint (0), + fc)) + out.title = pp_formatted_text (&pp); + } + out.kind.set_optional_value ("quickfix"); + out.isPreferred.set_optional_value (true); + out.edit.set_optional_value + (get_WorkspaceEdit_from_from_rich_location (rich_loc)); + + return true; +} + top_level_diagnostic lsp::gcc_impl::worker::diagnostics::sink:: make_top_level_diagnostic (const ::diagnostics::diagnostic_info &diagnostic, @@ -392,13 +463,13 @@ make_top_level_diagnostic (const ::diagnostics::diagnostic_info &diagnostic, (get_severity_for_kind (diagnostic.m_kind)); label_text option_text - = m_context.make_option_name (diagnostic.m_option_id, - orig_diag_kind, diagnostic.m_kind); + = m_context.get_option_name (diagnostic.m_option_id, + orig_diag_kind, diagnostic.m_kind); if (option_text.get ()) lsp_diag.code.set_optional_value (option_text.get ()); label_text option_url - = m_context.make_option_url (diagnostic.m_option_id); + = m_context.get_option_url (diagnostic.m_option_id); if (option_url.get ()) lsp_diag.codeDescription.set_optional_value ({option_url.get ()}); @@ -410,6 +481,15 @@ make_top_level_diagnostic (const ::diagnostics::diagnostic_info &diagnostic, pp_clear_output_area (pp); } + // FIXME: stash fix-it hints in the "data" + lsp::spec::CodeAction code_action; + if (maybe_get_CodeAction_from_from_rich_location + (*diagnostic.m_richloc, + get_context ().get_file_cache (), + code_action)) + lsp_diag.data.set_optional_value + (lsp::spec::LSPAny (code_action.to_json ())); + return {uri, std::move (lsp_diag)}; } diff --git a/gcc/lsp/gcc-impl/worker.cc b/gcc/lsp/gcc-impl/worker.cc index def78373cd1..1943c70dd54 100644 --- a/gcc/lsp/gcc-impl/worker.cc +++ b/gcc/lsp/gcc-impl/worker.cc @@ -38,6 +38,7 @@ along with GCC; see the file COPYING3. If not see #include "context.h" #include "channels.h" #include "uris.h" +#include "function.h" #if 0 #include "diagnostic.h" #include "diagnostics/color.h" @@ -48,6 +49,15 @@ along with GCC; see the file COPYING3. If not see // FIXME: extern struct cpp_reader* parse_in; +using ast_node = gcc::topics::ast_events::recorder::saved_node; + +static lsp::spec::Range +range_from_ast_node (const ast_node &node) +{ + return lsp::spec::Range::from_location_t_pair (node.m_start_loc, + node.m_end_loc); +} + // class lsp::gcc_impl::worker::mediator : public lsp::log_user lsp::gcc_impl::worker::mediator::mediator (lsp::logger *logger) @@ -61,7 +71,8 @@ lsp::gcc_impl::worker::mediator::mediator (lsp::logger *logger) LSP_LOG_SCOPE (logger); set_logger (logger); - m_server_ipc_connection.set_logger (logger); + if (1) + m_server_ipc_connection.set_logger (logger); m_server_ipc_connection.m_outgoing_sender.set_debug_name ("stdout from worker for server"); @@ -71,7 +82,8 @@ lsp::gcc_impl::worker::mediator::mediator (lsp::logger *logger) (&m_octets_from_server); m_octets_from_server.set_json_rpc_sink (this); - m_octets_from_server.set_logger (logger); + if (1) + m_octets_from_server.set_logger (logger); /* Record AST events. */ g->get_channels ().ast_events_channel.add_subscriber (m_recorder); @@ -83,7 +95,7 @@ lsp::gcc_impl::worker::mediator::mediator (lsp::logger *logger) (*global_dc, *this); m_lsp_diagnostic_sink = sink.get (); sink->update_printer (); -#if 1 +#if 0 global_dc->set_sink (std::move (sink)); #else global_dc->add_sink (std::move (sink)); @@ -102,7 +114,8 @@ lsp::gcc_impl::worker::mediator::handle_events_until_didOpen () while (1) { ipc::event_poll evp; - evp.set_logger (logger); + if (1) + evp.set_logger (logger); evp.add_endpoint (&m_server_ipc_connection.m_incoming_receiver); evp.add_endpoint (&m_server_ipc_connection.m_outgoing_sender); @@ -110,7 +123,27 @@ lsp::gcc_impl::worker::mediator::handle_events_until_didOpen () if (m_filename_from_didOpen) return m_filename_from_didOpen; - } + } +} + +static void +fixup_ast (ast_node &root_node) +{ + if (root_node.m_kind == nullptr + && root_node.m_children.size () == 1) + { + auto only_child = root_node.m_children[0].release (); + /* Make this be the root instead. */ + root_node = std::move (*only_child); + root_node.m_parent = nullptr; + for (auto &iter : root_node.m_children) + iter->m_parent = &root_node; + + if (root_node.m_end_loc == UNKNOWN_LOCATION + && root_node.m_children.size () > 0) + root_node.m_end_loc = root_node.m_children.back ()->m_end_loc; + + } } void @@ -125,8 +158,10 @@ handle_events_after_compile (lsp::frontend_worker_interface *frontend) m_frontend = frontend; + fixup_ast (m_recorder.m_root_node); + // FIXME: - //dump_ast (); + dump_ast (); // Flush any pending diagnostics gcc_assert (m_lsp_diagnostic_sink); @@ -143,12 +178,13 @@ handle_events_after_compile (lsp::frontend_worker_interface *frontend) while (1) { ipc::event_poll evp; - evp.set_logger (logger); + if (1) + evp.set_logger (logger); evp.add_endpoint (&m_server_ipc_connection.m_incoming_receiver); evp.add_endpoint (&m_server_ipc_connection.m_outgoing_sender); evp.handle_pending_events (true); - } + } } void @@ -189,7 +225,7 @@ consume_json_rpc_request (json_rpc::request &&req) } gcc_assert (m_frontend); - + #if 0 if (!req.get_params ()) { @@ -200,7 +236,70 @@ consume_json_rpc_request (json_rpc::request &&req) const char *method = req.get_method (); if (logger) logger->log ("call: \"%s\"", method); - if (0 == strcmp (method, "textDocument/documentSymbol")) + if (0 == strcmp (method, "textDocument/completion")) + { + auto unmarshal_result + = spec::CompletionParams::from_json (req.get_id (), + *req.get_params ()); + if (unmarshal_result.get_error ()) + { + gcc_unreachable (); // FIXME +#if 0 + send_response_to_client + (json_rpc::response::invalid_params (req, + std::move (unmarshal_result))); +#endif + return; + } + + on_request_textDocument_completion + (req.take_id (), + unmarshal_result.take_value ()); + return; + } + else if (0 == strcmp (method, "textDocument/declaration")) + { + auto unmarshal_result + = spec::DeclarationParams::from_json (req.get_id (), + *req.get_params ()); + if (unmarshal_result.get_error ()) + { + gcc_unreachable (); // FIXME +#if 0 + send_response_to_client + (json_rpc::response::invalid_params (req, + std::move (unmarshal_result))); +#endif + return; + } + + on_request_textDocument_declaration + (req.take_id (), + unmarshal_result.take_value ()); + return; + } + else if (0 == strcmp (method, "textDocument/definition")) + { + auto unmarshal_result + = spec::DefinitionParams::from_json (req.get_id (), + *req.get_params ()); + if (unmarshal_result.get_error ()) + { + gcc_unreachable (); // FIXME +#if 0 + send_response_to_client + (json_rpc::response::invalid_params (req, + std::move (unmarshal_result))); +#endif + return; + } + + on_request_textDocument_definition + (req.take_id (), + unmarshal_result.take_value ()); + return; + } + else if (0 == strcmp (method, "textDocument/documentSymbol")) { auto unmarshal_result = spec::DocumentSymbolParams::from_json (req.get_id (), @@ -221,10 +320,69 @@ consume_json_rpc_request (json_rpc::request &&req) unmarshal_result.take_value ()); return; } + else if (0 == strcmp (method, "textDocument/documentHighlight")) + { + auto unmarshal_result + = spec::DocumentHighlightParams::from_json (req.get_id (), + *req.get_params ()); + if (unmarshal_result.get_error ()) + { + gcc_unreachable (); // FIXME #if 0 - else if (0 == strcmp (method, "textDocument/definition")) - return dispatch_textDocument_definition (std::move (req)); + send_response_to_client + (json_rpc::response::invalid_params (req, + std::move (unmarshal_result))); +#endif + return; + } + + on_request_textDocument_documentHighlight + (req.take_id (), + unmarshal_result.take_value ()); + return; + } + else if (0 == strcmp (method, "textDocument/hover")) + { + auto unmarshal_result + = spec::HoverParams::from_json (req.get_id (), + *req.get_params ()); + if (unmarshal_result.get_error ()) + { + gcc_unreachable (); // FIXME +#if 0 + send_response_to_client + (json_rpc::response::invalid_params (req, + std::move (unmarshal_result))); #endif + return; + } + + on_request_textDocument_hover + (req.take_id (), + unmarshal_result.take_value ()); + return; + } + else if (0 == strcmp (method, "textDocument/signatureHelp")) + { + auto unmarshal_result + = spec::SignatureHelpParams::from_json (req.get_id (), + *req.get_params ()); + if (unmarshal_result.get_error ()) + { + gcc_unreachable (); // FIXME +#if 0 + send_response_to_client + (json_rpc::response::invalid_params (req, + std::move (unmarshal_result))); +#endif + return; + } + + on_request_textDocument_signatureHelp + (req.take_id (), + unmarshal_result.take_value ()); + return; + } send_response_to_server (json_rpc::response::method_not_found (std::move (req))); @@ -293,16 +451,272 @@ on_invalid_json_rpc (json_rpc::error &&) gcc_unreachable (); } +// If we've lost our connection to the server, exit. + void lsp::gcc_impl::worker::mediator:: on_finish () { - LSP_LOG_SCOPE (get_logger ()); - gcc_unreachable (); + auto logger = get_logger (); + LSP_LOG_SCOPE (logger); + if (logger) + logger->log ("exiting"); + + exit (0); } // Handling of requests from server +// FIXME: use this + +static bool +get_CompletionItemKind_for_tree (tree node, + lsp::spec::CompletionItemKind &out) +{ + switch (TREE_CODE (node)) + { + default: + return false; + case FUNCTION_DECL: + out.m_val = lsp::spec::CompletionItemKind::values::Function; + return true; + // FIXME + } +} + +void +lsp::gcc_impl::worker::mediator:: +on_request_textDocument_completion (std::unique_ptr req_id, + spec::CompletionParams &&p) +{ + auto logger = get_logger (); + LSP_LOG_SCOPE (logger); + + gcc_assert (m_frontend); + + //p.position.character.m_val -= 1; // FIXME + if (auto node = get_ast_node_at_position (p)) + { + // FIXME: + if (1) + { + fprintf (stderr, "\nnode at position: %i, %i:\n", + p.position.line, + p.position.character); + visit_node (*node); + } + + std::vector ancestry; + for (auto iter = node; iter; iter = iter->m_parent) + { + if (iter->m_expr) + ancestry.push_back (iter->m_expr); + } + + spec::CompletionList list; + if (m_frontend->get_CompletionList (ancestry, list)) + { + send_response_to_server + (json_rpc::response::success (std::move (req_id), + list.to_json ())); + return; + } + } + + send_response_to_server + (json_rpc::response::success (std::move (req_id), + json::make_null ())); +} + +void +lsp::gcc_impl::worker::mediator:: +on_request_textDocument_declaration (std::unique_ptr req_id, + spec::DeclarationParams &&p) +{ + auto logger = get_logger (); + LSP_LOG_SCOPE (logger); + + gcc_assert (m_frontend); + + if (auto node = get_ast_node_at_position (p)) + { + // FIXME: + if (0) + { + fprintf (stderr, "\nnode at position: \n"); + visit_node (*node); + } + if (node->m_expr) + { + if (DECL_P (node->m_expr)) + { + location_t loc = DECL_SOURCE_LOCATION (node->m_expr); + auto result (lsp::spec::Location::from_location_t (loc)); + send_response_to_server + (json_rpc::response::success (std::move (req_id), + result.to_json ())); + return; + } + } + else + { + if (logger) + logger->log ("ast node %s has no tree node", node->m_kind); + } + } + + if (logger) + logger->log ("ast node not found"); + send_response_to_server + (json_rpc::response::success (std::move (req_id), + json::make_null ())); +} + +static location_t +get_location_of_definition (tree decl) +{ + switch (TREE_CODE (decl)) + { + default: + // FIXME: this is getting at the decl, not the defn + return DECL_SOURCE_LOCATION (decl); + case FUNCTION_DECL: + { + function *fn = DECL_STRUCT_FUNCTION (decl); + return fn->function_start_locus; // FIXME + } + } +} + +void +lsp::gcc_impl::worker::mediator:: +on_request_textDocument_definition (std::unique_ptr req_id, + spec::DefinitionParams &&p) +{ + auto logger = get_logger (); + LSP_LOG_SCOPE (logger); + + gcc_assert (m_frontend); + + if (auto node = get_ast_node_at_position (p)) + { + // FIXME: + if (0) + { + fprintf (stderr, "\nnode at position: \n"); + visit_node (*node); + } + if (node->m_expr) + { + if (DECL_P (node->m_expr)) + { + location_t loc = get_location_of_definition (node->m_expr); + // FIXME: this is getting at the decl, not the defn + auto result (lsp::spec::Location::from_location_t (loc)); + send_response_to_server + (json_rpc::response::success (std::move (req_id), + result.to_json ())); + return; + } + } + else + { + if (logger) + logger->log ("ast node %s has no tree node", node->m_kind); + } + } + + if (logger) + logger->log ("ast node not found"); + send_response_to_server + (json_rpc::response::success (std::move (req_id), + json::make_null ())); +} +struct decl_usage_visitor +{ + decl_usage_visitor (tree decl) + : m_decl (decl) + { + gcc_assert (m_decl); + } + + void visit (ast_node &node) + { + if (node.m_expr == m_decl) + m_uses.push_back (&node); + } + + tree m_decl; + std::vector m_uses; +}; + +void +lsp::gcc_impl::worker::mediator:: +on_request_textDocument_documentHighlight (std::unique_ptr req_id, + spec::DocumentHighlightParams &&p) +{ + auto logger = get_logger (); + LSP_LOG_SCOPE (logger); + + gcc_assert (m_frontend); + + if (auto node = get_ast_node_at_position (p)) + { + // FIXME: + if (0) + { + fprintf (stderr, "\nnode at position: \n"); + visit_node (*node); + } + if (node->m_expr) + { + if (DECL_P (node->m_expr)) + { + spec::DocumentHighlightKind text_kind; + text_kind.m_val = spec::DocumentHighlightKind::values::Text; + + vector results; + + /* Get declaration. */ + { + spec::DocumentHighlight decl; + location_t loc = DECL_SOURCE_LOCATION (node->m_expr); + decl.range = lsp::spec::Range::from_location_t (loc); + decl.kind.set_optional_value (text_kind); + results.push_back (decl); + } + + /* Get uses. */ + decl_usage_visitor visitor (node->m_expr); + m_recorder.m_root_node.accept (visitor); + for (auto iter : visitor.m_uses) + { + spec::DocumentHighlight use; + use.range = range_from_ast_node (*iter); + use.kind.set_optional_value (text_kind); + results.push_back (use); + } + + send_response_to_server + (json_rpc::response::success (std::move (req_id), + results.to_json ())); + return; + } + } + else + { + if (logger) + logger->log ("ast node %s has no tree node", node->m_kind); + } + } + + if (logger) + logger->log ("ast node not found"); + send_response_to_server + (json_rpc::response::success (std::move (req_id), + json::make_null ())); +} + void lsp::gcc_impl::worker::mediator:: on_request_textDocument_documentSymbol (std::unique_ptr req_id, @@ -311,10 +725,6 @@ on_request_textDocument_documentSymbol (std::unique_ptr req_id, LSP_LOG_SCOPE (get_logger ()); gcc_assert (m_frontend); - /* FIXME: we can hit this with null frontend - if the request hits in the same loop as didOpen. - We need to stop processing the inputs immediately after - didOpen is processed. */ auto symbols = m_frontend->get_symbols (); send_response_to_server @@ -322,6 +732,112 @@ on_request_textDocument_documentSymbol (std::unique_ptr req_id, symbols.to_json ())); } +void +lsp::gcc_impl::worker::mediator:: +on_request_textDocument_hover (std::unique_ptr req_id, + spec::HoverParams &&p) +{ + auto logger = get_logger (); + LSP_LOG_SCOPE (logger); + + gcc_assert (m_frontend); + + if (auto node = get_ast_node_at_position (p)) + { + // FIXME: + if (0) + { + fprintf (stderr, "\nnode at position: \n"); + visit_node (*node); + } + if (node->m_expr) + { + lsp::spec::Hover result; + if (m_frontend->get_hover_text (node->m_expr, + result.contents)) + { + result.range.set_optional_value (range_from_ast_node (*node)); + send_response_to_server + (json_rpc::response::success (std::move (req_id), + result.to_json ())); + return; + } + else + { + if (logger) + logger->log ("frontend returned false"); + } + } + else + { + if (logger) + logger->log ("ast node %s has no tree node", node->m_kind); + } + } + else + { + if (logger) + logger->log ("ast node not found"); + } + send_response_to_server + (json_rpc::response::success (std::move (req_id), + json::make_null ())); +} + +bool +lsp::gcc_impl::worker::mediator:: +get_any_SignatureHelp_for_ast_node (const ast_node &node, + lsp::spec::SignatureHelp &out) +{ + /* Assume we have a postfix-expression with an identifier + as the first child. */ + if (node.m_children.size () > 0) + { + auto &first_child = node.m_children[0]; + if (0 == strcmp (first_child->m_kind, "identifier") + && first_child->m_expr + && TREE_CODE (first_child->m_expr) == FUNCTION_DECL) + { + if (m_frontend->get_SignatureHelp (first_child->m_expr, out)) + return true; + } + } + return false; +} + +void +lsp::gcc_impl::worker::mediator:: +on_request_textDocument_signatureHelp (std::unique_ptr req_id, + spec::SignatureHelpParams &&p) +{ + auto logger = get_logger (); + LSP_LOG_SCOPE (logger); + + gcc_assert (m_frontend); + + if (auto node = get_ast_node_at_position (p)) + { + // FIXME: + if (0) + { + fprintf (stderr, "\nnode at position: \n"); + visit_node (*node); + } + + spec::SignatureHelp help; + if (get_any_SignatureHelp_for_ast_node (*node, help)) + { + send_response_to_server + (json_rpc::response::success (std::move (req_id), + help.to_json ())); + return; + } + } + send_response_to_server + (json_rpc::response::success (std::move (req_id), + json::make_null ())); +} + // Handling of notifications from server void @@ -336,13 +852,15 @@ on_notification_textDocument_didOpen (lsp::spec::DidOpenTextDocumentParams &&p) auto file_uri = uris::parse_as_file_uri (p.textDocument.uri.m_val); if (!file_uri) { - // FIXME: - gcc_unreachable (); + if (logger) + logger->log ("failed to parse as file uri"); + return; } if (!file_uri->local_p ()) { - // FIXME: - gcc_unreachable (); + if (logger) + logger->log ("uri is not local"); + return; } m_filename_from_didOpen = xstrdup (file_uri->m_path_absolute.c_str ()); @@ -385,7 +903,7 @@ lsp::gcc_impl::worker::mediator::dump_ast () /* FIXME: this will send it as a diagnostic, which is probably not want we want, as it will get intercepted. */ // might need a custom context and sink? - + auto_diagnostic_group d; inform (UNKNOWN_LOCATION, "AST nodes"); for (auto &iter : m_recorder.m_root_node.m_children) @@ -394,11 +912,60 @@ lsp::gcc_impl::worker::mediator::dump_ast () void lsp::gcc_impl::worker::mediator:: -visit_node (const gcc::topics::ast_events::recorder::saved_node &node) +visit_node (const ast_node &node) { // FIXME: see notes for dump_ast auto_diagnostic_nesting_level sentinel; - inform (node.m_start_loc, "%qs", node.m_kind); + const char *kind = node.m_kind ? node.m_kind : "NULL"; + auto r = range_from_ast_node (node); + lsp::pp_element e (r); + if (node.m_expr) + inform (node.m_start_loc, "start of %qs: %qE: %e", kind, node.m_expr, &e); + else + inform (node.m_start_loc, "start of %qs: %e", kind, &e); for (auto &iter : node.m_children) visit_node (*iter); + if (node.m_expr) + inform (node.m_end_loc, "end of %qs: %qE: %e", kind, node.m_expr, &e); + else + inform (node.m_end_loc, "end of %qs: %e", kind, &e); +} + +static bool +node_contains_p (const ast_node &node, + const lsp::spec::TextDocumentPositionParams &p) +{ + lsp::spec::Range range_of_node = range_from_ast_node (node); + return range_of_node.contains_p (p.position); +} + +const ast_node * +lsp::gcc_impl::worker::mediator:: +get_ast_node_at_position (const lsp::spec::TextDocumentPositionParams &p) +{ + const ast_node *iter_node = &m_recorder.m_root_node; + + // FIXME: + if (0) + visit_node (m_recorder.m_root_node); + + /* Try to find the deepest descendent of the root node that + encloses the point. */ + const ast_node *current_node = &m_recorder.m_root_node; + while (true) + { + if (!node_contains_p (*current_node, p)) + return nullptr; // Something has gone wrong + + iterate: + for (auto &child_iter : current_node->m_children) + { + if (node_contains_p (*child_iter, p)) + { + current_node = child_iter.get (); + goto iterate; + } + } + return current_node; + } } diff --git a/gcc/lsp/gcc-impl/worker.h b/gcc/lsp/gcc-impl/worker.h index 5f87ee3718c..e45243a6028 100644 --- a/gcc/lsp/gcc-impl/worker.h +++ b/gcc/lsp/gcc-impl/worker.h @@ -105,10 +105,34 @@ public: // Handling of requests from server + void + on_request_textDocument_completion (std::unique_ptr req_id, + spec::CompletionParams &&p); + + void + on_request_textDocument_declaration (std::unique_ptr req_id, + spec::DeclarationParams &&p); + + void + on_request_textDocument_definition (std::unique_ptr req_id, + spec::DefinitionParams &&p); + + void + on_request_textDocument_documentHighlight (std::unique_ptr req_id, + spec::DocumentHighlightParams &&p); + void on_request_textDocument_documentSymbol (std::unique_ptr req_id, spec::DocumentSymbolParams &&p); + void + on_request_textDocument_hover (std::unique_ptr req_id, + spec::HoverParams &&p); + + void + on_request_textDocument_signatureHelp (std::unique_ptr req_id, + spec::SignatureHelpParams &&p); + // Handling of notifications from server void @@ -119,6 +143,13 @@ public: void dump_ast (); void visit_node (const gcc::topics::ast_events::recorder::saved_node &node); + bool + get_any_SignatureHelp_for_ast_node (const gcc::topics::ast_events::recorder::saved_node &node, + lsp::spec::SignatureHelp &out); + + const gcc::topics::ast_events::recorder::saved_node * + get_ast_node_at_position (const spec::TextDocumentPositionParams &p); + ipc::connection m_server_ipc_connection; octets_to_json_rpc m_octets_from_server; char *m_filename_from_didOpen; diff --git a/gcc/lsp/json-rpc.cc b/gcc/lsp/json-rpc.cc index 3f3ce881469..5b74ae09110 100644 --- a/gcc/lsp/json-rpc.cc +++ b/gcc/lsp/json-rpc.cc @@ -104,8 +104,11 @@ json_rpc::response json_rpc::response::success (std::unique_ptr req_id, std::unique_ptr result) { + gcc_assert (req_id); + gcc_assert (result); return response (std::move (result), std::move (req_id)); } + /* A C++ class for a JSON-RPC response. */ json_rpc::error @@ -231,8 +234,14 @@ consume_json_message (json::value &&jv_msg) { /* "method" and "id": this is a JSON-RPC request. */ if (logger) - logger->log ("JSON-RPC call(FIXME): \"%s\"", - method_str->get_string ()); + { + logger->start_log_line (); + logger->log_partial ("JSON-RPC request("); + req_id->dump (logger->get_file (), false); + logger->log_partial ("): \"%s\"", + method_str->get_string ()); + logger->end_log_line (); + } json_rpc::request req (method_str->get_string (), std::move (params), std::move (req_id)); @@ -254,13 +263,30 @@ consume_json_message (json::value &&jv_msg) { if (req_id.get ()) { - /* "id" without "method": this is a JSON-RPC response. */ + /* "id" without "method": this is a JSON-RPC response or error. */ if (logger) - logger->log ("JSON-RPC response(FIXME)"); - auto result = msg_obj.maybe_take ("result"); - auto resp = json_rpc::response::success (std::move (req_id), - std::move (result)); - consume_json_rpc_response (std::move (resp)); + { + logger->start_log_line (); + logger->log_partial ("JSON-RPC response("); + req_id->dump (logger->get_file (), false); + logger->log_partial (")"); + logger->end_log_line (); + } + if (auto result = msg_obj.maybe_take ("result")) + { + auto resp = json_rpc::response::success (std::move (req_id), + std::move (result)); + consume_json_rpc_response (std::move (resp)); + } + else if (auto error = msg_obj.maybe_take ("error")) + { + gcc_unreachable (); // FIXME +#if 0 + auto resp = json_rpc::error (std::move (req_id), + std::move (error)); + consume_json_rpc_response (std::move (resp)); +#endif + } } else { diff --git a/gcc/lsp/lsp-logging.cc b/gcc/lsp/lsp-logging.cc index f56a94dbbac..10c056acdb2 100644 --- a/gcc/lsp/lsp-logging.cc +++ b/gcc/lsp/lsp-logging.cc @@ -107,12 +107,39 @@ logger::log (const char *fmt, ...) void logger::log_va (const char *fmt, va_list ap) +{ + start_log_line (); + vfprintf (m_f_out, fmt, ap); + end_log_line (); +} + +void +logger::start_log_line () { emit_start_color (); fprintf (m_f_out, "%s: ", m_prefix.c_str ()); for (int i = 0; i < m_indent_level; i++) fputc (' ', m_f_out); - vfprintf (m_f_out, fmt, ap); +} + +void +logger::log_partial (const char *fmt, ...) +{ + va_list ap; + va_start (ap, fmt); + log_va_partial (fmt, &ap); + va_end (ap); +} + +void +logger::log_va_partial (const char *fmt, va_list *ap) +{ + vfprintf (m_f_out, fmt, *ap); +} + +void +logger::end_log_line () +{ fprintf (m_f_out, "\n"); emit_stop_color (); fflush (m_f_out); diff --git a/gcc/lsp/lsp-logging.h b/gcc/lsp/lsp-logging.h index 6da65a98894..85bb748878a 100644 --- a/gcc/lsp/lsp-logging.h +++ b/gcc/lsp/lsp-logging.h @@ -58,6 +58,12 @@ class logger GNU_PRINTF(2, 3); void log_va (const char *fmt, va_list ap) GNU_PRINTF(2, 0); + void start_log_line (); + void log_partial (const char *fmt, ...) + GNU_PRINTF(2, 3); + void log_va_partial (const char *fmt, va_list *ap) + GNU_PRINTF(2, 0); + void end_log_line (); void enter_scope (const char *scope_name); void exit_scope (const char *scope_name); @@ -65,6 +71,8 @@ class logger void emit_start_color (); void emit_stop_color (); + FILE *get_file () const { return m_f_out; } + private: int m_refcount; FILE *m_f_out; diff --git a/gcc/lsp/lsp-spec.cc b/gcc/lsp/lsp-spec.cc index 17191bc21b9..eb9283f1e90 100644 --- a/gcc/lsp/lsp-spec.cc +++ b/gcc/lsp/lsp-spec.cc @@ -1,5 +1,5 @@ /* Language Server Protocol implementation. - Copyright (C) 2017-2026 Free Software Foundation, Inc. + Copyright (C) 2015-2026 Free Software Foundation, Inc. Contributed by David Malcolm . This file is part of GCC. @@ -105,6 +105,17 @@ not_an_integer (const unmarshaller &ctxt, return ctxt.invalid_params (pp_formatted_text (&pp)); } +result +not_a_bool (const unmarshaller &ctxt, + const json::value &jv) +{ + pretty_printer pp; + pp_markup::quoted_json_pointer e_js_pointer (jv); + pp_printf (&pp, "expected boolean for %e; got %s", + &e_js_pointer, ctxt.describe_kind (jv)); + return ctxt.invalid_params (pp_formatted_text (&pp)); +} + template result missing_property (const unmarshaller &ctxt, @@ -150,6 +161,47 @@ unmarshal_optional_property (const unmarshaller &ctxt, return optional (from_json_result.take_value ()); } +/* template + class map : public std::map. */ + +template +result> +lsp::map::from_json (const unmarshaller &ctxt, + const json::value &jv) +{ + const json::object *js_obj = jv.dyn_cast_object (); + if (!js_obj) + { + gcc_unreachable (); // FIXME + //return not_an_object> (ctxt, jv); + } + map m; + for (auto iter : js_obj->get_map ()) + { + const char *k = iter.first; + json::value *v = iter.second; + auto v_result = MappedType::from_json (ctxt, *v); + if (v_result.get_error ()) + return v_result.take_error (); + m.insert ({KeyType (k), v_result.take_value ()}); + } + return m; +} + +template +std::unique_ptr +lsp::map::to_json () const +{ + auto json_obj = std::make_unique (); + for (auto iter : *this) + { + const KeyType &key (iter.first); + auto json_mapped_val = iter.second.to_json (); + json_obj->set (key.m_val.c_str (), std::move (json_mapped_val)); + } + return json_obj; +} + /* Macros for implementing demarshalling for a given type. */ /* FIXME. */ @@ -225,17 +277,65 @@ lsp::spec::TYPE_NAME::from_json (const unmarshaller &ctxt, \ return marshalled_obj; \ } +// FIXME: add generic code for this +// FIXME: check for known values? + +template +lsp::result +unmarshal_enum (const unmarshaller &ctxt, + const json::value &jv) +{ + const json::integer_number *js_int = jv.dyn_cast_integer_number (); + if (!js_int) + return not_an_integer (ctxt, jv); + + T unmarshalled_val; + unmarshalled_val.m_val = static_cast (js_int->get ()); + return unmarshalled_val; +} + +template +std::unique_ptr +marshal_enum (const T &t) +{ + return std::make_unique (static_cast (t.m_val)); +} // TODO: autogenerate the interface binding/marshalling/demarshalling code // from an interface description. +/* struct LSPAny. */ + +lsp::result +lsp::spec::LSPAny::from_json (const unmarshaller &ctxt, + const json::value &jv) +{ + LSPAny result; + result.m_val = jv.clone (); + return result; +} + +std::unique_ptr +lsp::spec::LSPAny::to_json () const +{ + return m_val->clone (); +} + /* struct boolean. */ lsp::result lsp::spec::boolean::from_json (const unmarshaller &ctxt, - const json::value &jv) + const json::value &jv) { - gcc_unreachable (); // FIXME + switch (jv.get_kind ()) + { + default: + return not_a_bool (ctxt, jv); + case json::JSON_TRUE: + return lsp::spec::boolean (true); + case json::JSON_FALSE: + return lsp::spec::boolean (false); + } } std::unique_ptr @@ -392,19 +492,114 @@ lsp::spec::InitializeParams::to_json () const gcc_unreachable (); // FIXME } +// struct CompletionOptions + +BEGIN_DEFINE_FROM_JSON(CompletionOptions) +{ + UNMARSHAL_OPTIONAL_PROPERTY (string, triggerCharacters); + UNMARSHAL_OPTIONAL_PROPERTY (string, allCommitCharacters); + UNMARSHAL_OPTIONAL_PROPERTY (boolean, resolveProvider); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(CompletionOptions) +{ + MARSHAL_OPTIONAL_PROPERTY (string, triggerCharacters); + MARSHAL_OPTIONAL_PROPERTY (string, allCommitCharacters); + MARSHAL_OPTIONAL_PROPERTY (boolean, resolveProvider); +} +END_DEFINE_TO_JSON + +// struct HoverOptions + +BEGIN_DEFINE_FROM_JSON(HoverOptions) +{ + // empty +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(HoverOptions) +{ + // empty +} +END_DEFINE_TO_JSON + +// struct SignatureHelpOptions + +BEGIN_DEFINE_FROM_JSON(SignatureHelpOptions) +{ + UNMARSHAL_OPTIONAL_PROPERTY (vector, triggerCharacters); + UNMARSHAL_OPTIONAL_PROPERTY (vector, retriggerCharacters); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(SignatureHelpOptions) +{ + MARSHAL_OPTIONAL_PROPERTY (vector, triggerCharacters); + MARSHAL_OPTIONAL_PROPERTY (vector, retriggerCharacters); +} +END_DEFINE_TO_JSON + +// struct SemanticTokensLegend + +BEGIN_DEFINE_FROM_JSON(SemanticTokensLegend) +{ + UNMARSHAL_REQUIRED_PROPERTY (vector, tokenTypes); + UNMARSHAL_REQUIRED_PROPERTY (vector, tokenModifiers); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(SemanticTokensLegend) +{ + MARSHAL_REQUIRED_PROPERTY (vector, tokenTypes); + MARSHAL_REQUIRED_PROPERTY (vector, tokenModifiers); +} +END_DEFINE_TO_JSON + +// struct SemanticTokensOptions + +BEGIN_DEFINE_FROM_JSON(SemanticTokensOptions) +{ + UNMARSHAL_REQUIRED_PROPERTY (SemanticTokensLegend, legend); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(SemanticTokensOptions) +{ + MARSHAL_REQUIRED_PROPERTY (SemanticTokensLegend, legend); +} +END_DEFINE_TO_JSON + // struct ServerCapabilities BEGIN_DEFINE_FROM_JSON(ServerCapabilities) { // FIXME - //UNMARSHAL_OPTIONAL_PROPERTY (boolean, documentSymbolProvider); + + UNMARSHAL_OPTIONAL_PROPERTY (CompletionOptions, completionProvider); + UNMARSHAL_OPTIONAL_PROPERTY (HoverOptions, hoverProvider); + UNMARSHAL_OPTIONAL_PROPERTY (SignatureHelpOptions, signatureHelpProvider); + UNMARSHAL_OPTIONAL_PROPERTY (boolean, declarationProvider); + UNMARSHAL_OPTIONAL_PROPERTY (boolean, definitionProvider); + UNMARSHAL_OPTIONAL_PROPERTY (boolean, documentHighlightProvider); + UNMARSHAL_OPTIONAL_PROPERTY (boolean, documentSymbolProvider); + UNMARSHAL_OPTIONAL_PROPERTY (boolean, codeActionProvider); + UNMARSHAL_OPTIONAL_PROPERTY (SemanticTokensOptions, semanticTokensProvider); } END_DEFINE_FROM_JSON BEGIN_DEFINE_TO_JSON(ServerCapabilities) { // FIXME + MARSHAL_OPTIONAL_PROPERTY (CompletionOptions, completionProvider); + MARSHAL_OPTIONAL_PROPERTY (HoverOptions, hoverProvider); + MARSHAL_OPTIONAL_PROPERTY (SignatureHelpOptions, signatureHelpProvider); + MARSHAL_OPTIONAL_PROPERTY (boolean, declarationProvider); + MARSHAL_OPTIONAL_PROPERTY (boolean, definitionProvider); + MARSHAL_OPTIONAL_PROPERTY (boolean, documentHighlightProvider); MARSHAL_OPTIONAL_PROPERTY (boolean, documentSymbolProvider); + MARSHAL_OPTIONAL_PROPERTY (boolean, codeActionProvider); + MARSHAL_OPTIONAL_PROPERTY (SemanticTokensOptions, semanticTokensProvider); } END_DEFINE_TO_JSON @@ -473,6 +668,13 @@ lsp::spec::Position::to_json () const return jobj; } +lsp::spec::Position +lsp::spec::Position::from_location_t (location_t loc) +{ + expanded_location exp_loc = expand_location (loc); + return from_expanded_location (exp_loc); +} + lsp::spec::Position lsp::spec::Position::from_expanded_location (const expanded_location &exp_loc) { @@ -507,6 +709,12 @@ lsp::spec::Range::from_location_t (location_t loc) { location_t start = get_start (loc); location_t finish = get_finish (loc); + return from_location_t_pair (start, finish); +} + +lsp::spec::Range +lsp::spec::Range::from_location_t_pair (location_t start, location_t finish) +{ expanded_location exp_loc_start = expand_location (start); expanded_location exp_loc_finish = expand_location (finish); @@ -520,6 +728,100 @@ lsp::spec::Range::from_location_t (location_t loc) return result; } +/* Compare with layout_range::contains_point, except that here + ranges are half-open, and we don't have column units. + // FIXME: maybe we do? + + Example A: a single-line range: + start: (col=22, line=2) + end: (col=39, line=2) + + |00000001111111111222222222233333333334444444444 + |34567890123456789012345678901234567890123456789 +--+----------------------------------------------- +01|bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +02|bbbbbbbbbbbbbbbbbbbSwwwwwwwwwwwwwwwFEaaaaaaaaaa +03|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + + Example B: a multiline range with + start: (col=14, line=3) + end: (col=09, line=5) + + |00000001111111111222222222233333333334444444444 + |34567890123456789012345678901234567890123456789 +--+----------------------------------------------- +01|bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +02|bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb +03|bbbbbbbbbbbSwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww +04|wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww +05|wwwwwFEaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +06|aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +--+----------------------------------------------- + + Legend: + - 'b' indicates a point *before* the range + - 'S' indicates the start of the range + - 'w' indicates a point within the range + - 'F' indicates the finish of the range (which is + within it). + - 'E' indicates the end of the range (which is immediately + after it). + - 'a' indicates a subsequent point *after* the range. */ + +// FIXME: unit tests + +bool +lsp::spec::Range::contains_p (const lsp::spec::Position &p) const +{ + gcc_assert (start.line <= end.line); + + if (p.line < start.line) + /* Points before the first line of the range are + outside it (corresponding to line 01 in example A + and lines 01 and 02 in example B above). */ + return false; + if (p.line == start.line) + { + /* On same line as start of range (corresponding + to line 02 in example A and line 03 in example B). */ + if (p.character < start.character) + /* Points on the starting line of the range, but + before the character in which it begins. */ + return false; + if (p.line < end.line) + /* This is a multiline range; the point + is within it (corresponds to line 03 in example B + from character 14 onwards) */ + return true; + /* This is a single-line range. */ + gcc_assert (p.line == end.line); + return p.character < end.character; + } + + /* The point is in a line beyond that containing the + start of the range: lines 03 onwards in example A, + and lines 04 onwards in example B. */ + gcc_assert (p.line > start.line); + + if (p.line > end.line) + /* The point is beyond the final line of the range + (lines 03 onwards in example A, and lines 06 onwards + in example B). */ + return false; + + if (p.line < end.line) + { + /* The point is in a line that's fully within a multiline + range (e.g. line 04 in example B). */ + gcc_assert (start.line < end.line); + return true; + } + + gcc_assert (p.line == end.line); + + return p.character < end.character; +} + /* struct Location. */ BEGIN_DEFINE_TO_JSON(Location) @@ -681,10 +983,17 @@ lsp::spec::TextDocumentPositionParams::from_json (const unmarshaller &ctxt, // struct DiagnosticSeverity +lsp::result +lsp::spec::DiagnosticSeverity::from_json (const unmarshaller &ctxt, + const json::value &jv) +{ + return unmarshal_enum (ctxt, jv); +} + std::unique_ptr lsp::spec::DiagnosticSeverity::to_json () const { - return std::make_unique (m_val); + return marshal_enum (*this); } // struct DiagnosticRelatedInformation @@ -719,25 +1028,35 @@ END_DEFINE_TO_JSON // struct Diagnostic -std::unique_ptr -lsp::spec::Diagnostic::to_json () const +BEGIN_DEFINE_FROM_JSON(Diagnostic) { - auto jobj = std::make_unique (); - jobj->set ("range", range.to_json ()); - if (auto val = severity.get_optional_value ()) - jobj->set ("severity", val->to_json ()); - if (auto val = code.get_optional_value ()) - jobj->set ("code", val->to_json ()); - if (auto val = codeDescription.get_optional_value ()) - jobj->set ("codeDescription", val->to_json ()); + UNMARSHAL_REQUIRED_PROPERTY (Range, range); + UNMARSHAL_OPTIONAL_PROPERTY (DiagnosticSeverity, severity); + UNMARSHAL_OPTIONAL_PROPERTY (string, code); + UNMARSHAL_OPTIONAL_PROPERTY (CodeDescription, codeDescription); - jobj->set ("message", message.to_json ()); + UNMARSHAL_REQUIRED_PROPERTY (string, message); - if (auto val = relatedInformation.get_optional_value ()) - jobj->set ("relatedInformation", val->to_json ()); + UNMARSHAL_OPTIONAL_PROPERTY (vector, + relatedInformation); + UNMARSHAL_OPTIONAL_PROPERTY (LSPAny, data); +} +END_DEFINE_FROM_JSON - return jobj; +BEGIN_DEFINE_TO_JSON(Diagnostic) +{ + MARSHAL_REQUIRED_PROPERTY (Range, range); + MARSHAL_OPTIONAL_PROPERTY (DiagnosticSeverity, severity); + MARSHAL_OPTIONAL_PROPERTY (string, severity); + MARSHAL_OPTIONAL_PROPERTY (CodeDescription, codeDescription); + + MARSHAL_REQUIRED_PROPERTY (string, message); + + MARSHAL_OPTIONAL_PROPERTY (vector, + relatedInformation); + MARSHAL_OPTIONAL_PROPERTY (LSPAny, data); } +END_DEFINE_TO_JSON // struct PublishDiagnosticsParams @@ -753,6 +1072,105 @@ lsp::spec::PublishDiagnosticsParams::to_json () const return jobj; } +// struct CompletionParams : public TextDocumentPositionParams + +BEGIN_DEFINE_FROM_JSON(CompletionParams) +{ + // FIXME: chain up to parent + UNMARSHAL_REQUIRED_PROPERTY (TextDocumentIdentifier, textDocument); + UNMARSHAL_REQUIRED_PROPERTY (Position, position); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(CompletionParams) +{ + // FIXME: chain up to parent + MARSHAL_REQUIRED_PROPERTY (TextDocumentIdentifier, textDocument); + MARSHAL_REQUIRED_PROPERTY (Position, position); +} +END_DEFINE_TO_JSON + +// struct CompletionItemKind + +lsp::result +lsp::spec::CompletionItemKind::from_json (const unmarshaller &ctxt, + const json::value &jv) +{ + return unmarshal_enum (ctxt, jv); +} + +std::unique_ptr +lsp::spec::CompletionItemKind::to_json () const +{ + return marshal_enum (*this); +} + +// struct CompletionItem + +BEGIN_DEFINE_FROM_JSON(CompletionItem) +{ + UNMARSHAL_REQUIRED_PROPERTY (string, label); + UNMARSHAL_OPTIONAL_PROPERTY (CompletionItemKind, kind); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(CompletionItem) +{ + MARSHAL_REQUIRED_PROPERTY (string, label); + MARSHAL_OPTIONAL_PROPERTY (CompletionItemKind, kind); +} +END_DEFINE_TO_JSON + +// struct CompletionList + +BEGIN_DEFINE_FROM_JSON(CompletionList) +{ + UNMARSHAL_REQUIRED_PROPERTY (vector, items); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(CompletionList) +{ + MARSHAL_REQUIRED_PROPERTY (vector, items); +} +END_DEFINE_TO_JSON + +// struct DeclarationParams : public TextDocumentPositionParams + +BEGIN_DEFINE_FROM_JSON(DeclarationParams) +{ + // FIXME: chain up to parent + UNMARSHAL_REQUIRED_PROPERTY (TextDocumentIdentifier, textDocument); + UNMARSHAL_REQUIRED_PROPERTY (Position, position); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(DeclarationParams) +{ + // FIXME: chain up to parent + MARSHAL_REQUIRED_PROPERTY (TextDocumentIdentifier, textDocument); + MARSHAL_REQUIRED_PROPERTY (Position, position); +} +END_DEFINE_TO_JSON + +// struct DefinitionParams : public TextDocumentPositionParams + +BEGIN_DEFINE_FROM_JSON(DefinitionParams) +{ + // FIXME: chain up to parent + UNMARSHAL_REQUIRED_PROPERTY (TextDocumentIdentifier, textDocument); + UNMARSHAL_REQUIRED_PROPERTY (Position, position); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(DefinitionParams) +{ + // FIXME: chain up to parent + MARSHAL_REQUIRED_PROPERTY (TextDocumentIdentifier, textDocument); + MARSHAL_REQUIRED_PROPERTY (Position, position); +} +END_DEFINE_TO_JSON + // struct DocumentSymbolParams BEGIN_DEFINE_FROM_JSON(DocumentSymbolParams) @@ -805,6 +1223,287 @@ BEGIN_DEFINE_TO_JSON(DocumentSymbol) } END_DEFINE_TO_JSON +// struct MarkupContent + +BEGIN_DEFINE_FROM_JSON(MarkupContent) +{ + UNMARSHAL_REQUIRED_PROPERTY (string, kind); + UNMARSHAL_REQUIRED_PROPERTY (string, value); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(MarkupContent) +{ + MARSHAL_REQUIRED_PROPERTY (string, kind); + MARSHAL_REQUIRED_PROPERTY (string, value); +} +END_DEFINE_TO_JSON + +// struct CodeActionTriggerKind + +lsp::result +lsp::spec::CodeActionTriggerKind::from_json (const unmarshaller &ctxt, + const json::value &jv) +{ + return unmarshal_enum (ctxt, jv); +} + +std::unique_ptr +lsp::spec::CodeActionTriggerKind::to_json () const +{ + return marshal_enum (*this); +} + +// struct CodeActionContext + +BEGIN_DEFINE_FROM_JSON(CodeActionContext) +{ + UNMARSHAL_REQUIRED_PROPERTY (vector, diagnostics); + UNMARSHAL_OPTIONAL_PROPERTY (vector, only); + UNMARSHAL_OPTIONAL_PROPERTY (CodeActionTriggerKind, triggerKind); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(CodeActionContext) +{ + MARSHAL_REQUIRED_PROPERTY (vector, diagnostics); + MARSHAL_OPTIONAL_PROPERTY (vector, only); + MARSHAL_OPTIONAL_PROPERTY (CodeActionTriggerKind, triggerKind); +} +END_DEFINE_TO_JSON + + +// struct CodeActionParams + +BEGIN_DEFINE_FROM_JSON(CodeActionParams) +{ + UNMARSHAL_REQUIRED_PROPERTY (TextDocumentIdentifier, textDocument); + UNMARSHAL_REQUIRED_PROPERTY (Range, range); + UNMARSHAL_REQUIRED_PROPERTY (CodeActionContext, context); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(CodeActionParams) +{ + MARSHAL_REQUIRED_PROPERTY (TextDocumentIdentifier, textDocument); + MARSHAL_REQUIRED_PROPERTY (Range, range); + MARSHAL_REQUIRED_PROPERTY (CodeActionContext, context); +} +END_DEFINE_TO_JSON + +// struct TextEdit + +BEGIN_DEFINE_FROM_JSON(TextEdit) +{ + UNMARSHAL_REQUIRED_PROPERTY (Range, range); + UNMARSHAL_REQUIRED_PROPERTY (string, newText); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(TextEdit) +{ + MARSHAL_REQUIRED_PROPERTY (Range, range); + MARSHAL_REQUIRED_PROPERTY (string, newText); +} +END_DEFINE_TO_JSON + +// struct WorkspaceEdit + +BEGIN_DEFINE_FROM_JSON(WorkspaceEdit) +{ + UNMARSHAL_OPTIONAL_PROPERTY (map_DocumentUri_to_vector_TextEdit, changes); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(WorkspaceEdit) +{ + MARSHAL_OPTIONAL_PROPERTY (map_DocumentUri_to_vector_TextEdit, changes); +} +END_DEFINE_TO_JSON + +// struct CodeAction + +BEGIN_DEFINE_FROM_JSON(CodeAction) +{ + UNMARSHAL_REQUIRED_PROPERTY (string, title); + UNMARSHAL_OPTIONAL_PROPERTY (CodeActionKind, kind); + UNMARSHAL_OPTIONAL_PROPERTY (vector, diagnostics); + UNMARSHAL_OPTIONAL_PROPERTY (boolean, isPreferred); + + UNMARSHAL_OPTIONAL_PROPERTY (WorkspaceEdit, edit); + + UNMARSHAL_OPTIONAL_PROPERTY (LSPAny, data); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(CodeAction) +{ + MARSHAL_REQUIRED_PROPERTY (string, title); + MARSHAL_OPTIONAL_PROPERTY (CodeActionKind, kind); + MARSHAL_OPTIONAL_PROPERTY (vector, diagnostics); + MARSHAL_OPTIONAL_PROPERTY (boolean, isPreferred); + + MARSHAL_OPTIONAL_PROPERTY (WorkspaceEdit, edit); + + MARSHAL_OPTIONAL_PROPERTY (LSPAny, data); +} +END_DEFINE_TO_JSON + +// struct HoverParams : public TextDocumentPositionParams + +BEGIN_DEFINE_FROM_JSON(HoverParams) +{ + // FIXME: how to share impl with base class? + UNMARSHAL_REQUIRED_PROPERTY (TextDocumentIdentifier, textDocument); + UNMARSHAL_REQUIRED_PROPERTY (Position, position); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(HoverParams) +{ + // FIXME: how to share impl with base class? + MARSHAL_REQUIRED_PROPERTY (TextDocumentIdentifier, textDocument); + MARSHAL_REQUIRED_PROPERTY (Position, position); +} +END_DEFINE_TO_JSON + +// struct Hover + +BEGIN_DEFINE_FROM_JSON(Hover) +{ + UNMARSHAL_REQUIRED_PROPERTY (MarkupContent, contents); + UNMARSHAL_OPTIONAL_PROPERTY (Range, range); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(Hover) +{ + MARSHAL_REQUIRED_PROPERTY (MarkupContent, contents); + MARSHAL_OPTIONAL_PROPERTY (Range, range); +} +END_DEFINE_TO_JSON + +// struct DocumentHighlightParams : public TextDocumentPositionParams + +BEGIN_DEFINE_FROM_JSON(DocumentHighlightParams) +{ + // FIXME: how to share impl with base class? + UNMARSHAL_REQUIRED_PROPERTY (TextDocumentIdentifier, textDocument); + UNMARSHAL_REQUIRED_PROPERTY (Position, position); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(DocumentHighlightParams) +{ + // FIXME: how to share impl with base class? + MARSHAL_REQUIRED_PROPERTY (TextDocumentIdentifier, textDocument); + MARSHAL_REQUIRED_PROPERTY (Position, position); +} +END_DEFINE_TO_JSON + +// struct DocumentHighlightKind + +lsp::result +lsp::spec::DocumentHighlightKind::from_json (const unmarshaller &ctxt, + const json::value &jv) +{ + return unmarshal_enum (ctxt, jv); +} + +std::unique_ptr +lsp::spec::DocumentHighlightKind::to_json () const +{ + return marshal_enum (*this); +} + +// struct DocumentHighlight + +BEGIN_DEFINE_FROM_JSON(DocumentHighlight) +{ + UNMARSHAL_REQUIRED_PROPERTY (Range, range); + UNMARSHAL_OPTIONAL_PROPERTY (DocumentHighlightKind, kind); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(DocumentHighlight) +{ + MARSHAL_REQUIRED_PROPERTY (Range, range); + MARSHAL_OPTIONAL_PROPERTY (DocumentHighlightKind, kind); +} +END_DEFINE_TO_JSON + +// struct SignatureHelpParams : public TextDocumentPositionParams + +BEGIN_DEFINE_FROM_JSON(SignatureHelpParams) +{ + // FIXME: how to share impl with base class? + UNMARSHAL_REQUIRED_PROPERTY (TextDocumentIdentifier, textDocument); + UNMARSHAL_REQUIRED_PROPERTY (Position, position); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(SignatureHelpParams) +{ + // FIXME: how to share impl with base class? + MARSHAL_REQUIRED_PROPERTY (TextDocumentIdentifier, textDocument); + MARSHAL_REQUIRED_PROPERTY (Position, position); +} +END_DEFINE_TO_JSON + +// struct ParameterInformation + +BEGIN_DEFINE_FROM_JSON(ParameterInformation) +{ + UNMARSHAL_REQUIRED_PROPERTY (string, label); + UNMARSHAL_OPTIONAL_PROPERTY (MarkupContent, documentation); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(ParameterInformation) +{ + MARSHAL_REQUIRED_PROPERTY (string, label); + MARSHAL_OPTIONAL_PROPERTY (MarkupContent, documentation); +} +END_DEFINE_TO_JSON + +// struct SignatureInformation + +BEGIN_DEFINE_FROM_JSON(SignatureInformation) +{ + UNMARSHAL_REQUIRED_PROPERTY (string, label); + UNMARSHAL_OPTIONAL_PROPERTY (MarkupContent, documentation); + UNMARSHAL_OPTIONAL_PROPERTY (vector, parameters); + UNMARSHAL_OPTIONAL_PROPERTY (uinteger, activeParameter); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(SignatureInformation) +{ + MARSHAL_REQUIRED_PROPERTY (string, label); + MARSHAL_OPTIONAL_PROPERTY (MarkupContent, documentation); + MARSHAL_OPTIONAL_PROPERTY (vector, parameters); + MARSHAL_OPTIONAL_PROPERTY (uinteger, activeParameter); +} +END_DEFINE_TO_JSON + +// struct SignatureHelp + +BEGIN_DEFINE_FROM_JSON(SignatureHelp) +{ + UNMARSHAL_REQUIRED_PROPERTY (vector, signatures); + UNMARSHAL_OPTIONAL_PROPERTY (uinteger, activeSignature); + UNMARSHAL_OPTIONAL_PROPERTY (uinteger, activeParameter); +} +END_DEFINE_FROM_JSON + +BEGIN_DEFINE_TO_JSON(SignatureHelp) +{ + MARSHAL_REQUIRED_PROPERTY (vector, signatures); + MARSHAL_OPTIONAL_PROPERTY (uinteger, activeSignature); + MARSHAL_OPTIONAL_PROPERTY (uinteger, activeParameter); +} +END_DEFINE_TO_JSON + #if CHECKING_P namespace selftest { @@ -883,6 +1582,109 @@ test_unmarshal_DocumentUri_ok () ASSERT_STREQ (r.take_value ().m_val.c_str (), "foo.c"); } +static void +test_range_contains_p_for_single_point () +{ + lsp::spec::Range r {{7, 10}, {7, 11}}; + + using Position = lsp::spec::Position; + + /* Before the line. */ + ASSERT_FALSE (r.contains_p (Position {6, 1})); + + /* On the line, but before start. */ + ASSERT_FALSE (r.contains_p (Position {7, 9})); + + /* At the point. */ + ASSERT_TRUE (r.contains_p (Position {7, 10})); + + /* On the line, after the point. */ + ASSERT_FALSE (r.contains_p (Position {7, 11})); + + /* After the line. */ + ASSERT_FALSE (r.contains_p (Position {8, 1})); +} + +static void +test_range_contains_p_for_single_line () +{ + lsp::spec::Range example_a {{2, 22}, {2, 39}}; + + using Position = lsp::spec::Position; + + /* Before the line. */ + ASSERT_FALSE (example_a.contains_p (Position {1, 1})); + + /* On the line, but before start. */ + ASSERT_FALSE (example_a.contains_p (Position {2, 21})); + + /* On the line, at the start. */ + ASSERT_TRUE (example_a.contains_p (Position {2, 22})); + + /* On the line, within the range. */ + ASSERT_TRUE (example_a.contains_p (Position {2, 23})); + + /* On the line, at the end. */ + ASSERT_TRUE (example_a.contains_p (Position {2, 38})); + + /* On the line, after the end. */ + ASSERT_FALSE (example_a.contains_p (Position {2, 39})); + + /* After the line. */ + ASSERT_FALSE (example_a.contains_p (Position {3, 39})); +} + +static void +test_range_contains_p_for_multiple_lines () +{ + lsp::spec::Range example_b {{3, 14}, {5, 9}}; + + using Position = lsp::spec::Position; + + /* Before first line. */ + ASSERT_FALSE (example_b.contains_p (Position {1, 1})); + + /* On the first line, but before start. */ + ASSERT_FALSE (example_b.contains_p (Position {3, 13})); + + /* At the start. */ + ASSERT_TRUE (example_b.contains_p (Position {3, 14})); + + /* On the first line, within the range. */ + ASSERT_TRUE (example_b.contains_p (Position {3, 15})); + + /* On an interior line. + The column number should not matter; try various boundary + values. */ + ASSERT_TRUE (example_b.contains_p (Position {4, 1})); + ASSERT_TRUE (example_b.contains_p (Position {4, 7})); + ASSERT_TRUE (example_b.contains_p (Position {4, 8})); + ASSERT_TRUE (example_b.contains_p (Position {4, 9})); + ASSERT_TRUE (example_b.contains_p (Position {4, 13})); + ASSERT_TRUE (example_b.contains_p (Position {4, 14})); + ASSERT_TRUE (example_b.contains_p (Position {4, 15})); + + /* On the final line, before the end. */ + ASSERT_TRUE (example_b.contains_p (Position {5, 7})); + + /* On the final line, at the end. */ + ASSERT_TRUE (example_b.contains_p (Position {5, 8})); + + /* On the final line, after the end. */ + ASSERT_FALSE (example_b.contains_p (Position {5, 9})); + + /* After the line. */ + ASSERT_FALSE (example_b.contains_p (Position {6, 1})); +} + +static void +test_range_contains_p () +{ + test_range_contains_p_for_single_point (); + test_range_contains_p_for_single_line (); + test_range_contains_p_for_multiple_lines (); +} + static void test_unmarshal_Position_missing_params () { @@ -935,11 +1737,13 @@ lsp_spec_cc_tests () test_unmarshal_integer_ok (); test_unmarshal_DocumentUri_not_a_string (); test_unmarshal_DocumentUri_ok (); + test_range_contains_p (); test_unmarshal_Position_missing_params (); #if 0 test_unmarshal_TextDocumentPositionParams_error (); #endif test_unmarshal_TextDocumentPositionParams_ok (); + } } // namespace selftest diff --git a/gcc/lsp/lsp-spec.h b/gcc/lsp/lsp-spec.h index efc4fdc7138..8282f83e80a 100644 --- a/gcc/lsp/lsp-spec.h +++ b/gcc/lsp/lsp-spec.h @@ -137,6 +137,29 @@ public: : m_ptr (std::make_unique (std::move (t))) {} + optional (const optional &other) + { + if (other.m_ptr) + m_ptr = std::make_unique (*other.m_ptr); + } + + optional & + operator= (const optional &other) + { + if (other.m_ptr) + m_ptr = std::make_unique (*other.m_ptr); + else + m_ptr.release (); + return *this; + } + + optional & + operator= (optional &&other) + { + m_ptr = std::move (other.m_ptr); + return *this; + } + Type * get_optional_value () { return m_ptr.get (); } @@ -148,6 +171,11 @@ public: { m_ptr = std::make_unique (std::move (t)); } + void + set_optional_value (const Type &t) + { + m_ptr = std::make_unique (t); + } private: std::unique_ptr m_ptr; @@ -198,6 +226,18 @@ public: } }; +template +class map : public std::map +{ +public: + static result> + from_json (const unmarshaller &ctxt, + const json::value &jv); + + std::unique_ptr + to_json () const; +}; + namespace spec { #define DECLARE_JSON_METHODS(TYPENAME) \ @@ -211,6 +251,24 @@ namespace spec { /* Types from the LSP protocol specification. This uses camel case, and so we do here, within this namespace, at least. */ +struct LSPAny +{ + DECLARE_JSON_METHODS(LSPAny) + + LSPAny () = default; + LSPAny (const LSPAny &other) + { + if (other.m_val) + m_val = other.m_val->clone (); + } + LSPAny (std::unique_ptr val) + : m_val (std::move (val)) + { + } + + std::unique_ptr m_val; +}; + // TODO: autogenerate the interface binding/marshalling/demarshalling code // from an interface description. @@ -229,6 +287,28 @@ struct uinteger DECLARE_JSON_METHODS(uinteger) uinteger () : m_val (0) {} + uinteger (unsigned val) : m_val (val) {} + + bool + operator< (const uinteger &other) const + { + return m_val < other.m_val; + } + bool + operator<= (const uinteger &other) const + { + return m_val <= other.m_val; + } + bool + operator== (const uinteger &other) const + { + return m_val == other.m_val; + } + bool + operator> (const uinteger &other) const + { + return m_val > other.m_val; + } unsigned m_val; }; @@ -254,6 +334,7 @@ struct string string () = default; string (const char *s) : m_val (s) {} + string (std::string s) : m_val (std::move (s)) {} std::string m_val; }; @@ -262,6 +343,9 @@ struct DocumentUri : public string { DECLARE_JSON_METHODS(DocumentUri) + DocumentUri () = default; + DocumentUri (std::string s) : string (std::move (s)) {} + bool operator< (const DocumentUri &other) const { @@ -310,13 +394,305 @@ struct InitializeParams // FIXME : public WorkDoneProgressParams // FIXME }; +struct CompletionOptions +{ + DECLARE_JSON_METHODS(CompletionOptions); + + optional triggerCharacters; + optional allCommitCharacters; + optional resolveProvider; + + // FIXME: +#if 0 + /** + * The server supports the following `CompletionItem` specific + * capabilities. + * + * @since 3.17.0 + */ + completionItem?: { + /** + * The server has support for completion item label + * details (see also `CompletionItemLabelDetails`) when receiving + * a completion item in a resolve call. + * + * @since 3.17.0 + */ + labelDetailsSupport?: boolean; + } +#endif +}; + +struct HoverOptions +{ + DECLARE_JSON_METHODS(HoverOptions); + // empty +}; + +struct SignatureHelpOptions +{ + DECLARE_JSON_METHODS(SignatureHelpOptions); + + optional> triggerCharacters; + optional> retriggerCharacters; +}; + +struct SemanticTokensLegend +{ + DECLARE_JSON_METHODS(SemanticTokensLegend); + + vector tokenTypes; + vector tokenModifiers; +}; + +struct SemanticTokensOptions +{ + DECLARE_JSON_METHODS(SemanticTokensOptions); + + SemanticTokensLegend legend; +}; + struct ServerCapabilities { DECLARE_JSON_METHODS(ServerCapabilities) // FIXME: +#if 0 + positionEncoding?: PositionEncodingKind; + + /** + * Defines how text documents are synced. Is either a detailed structure + * defining each notification or for backwards compatibility the + * TextDocumentSyncKind number. If omitted it defaults to + * `TextDocumentSyncKind.None`. + */ + textDocumentSync?: TextDocumentSyncOptions | TextDocumentSyncKind; + + /** + * Defines how notebook documents are synced. + * + * @since 3.17.0 + */ + notebookDocumentSync?: NotebookDocumentSyncOptions + | NotebookDocumentSyncRegistrationOptions; +#endif + + optional completionProvider; + optional hoverProvider; + optional signatureHelpProvider; + optional declarationProvider; + optional definitionProvider; + +#if 0 + /** + * The server provides goto type definition support. + * + * @since 3.6.0 + */ + typeDefinitionProvider?: boolean | TypeDefinitionOptions + | TypeDefinitionRegistrationOptions; + + /** + * The server provides goto implementation support. + * + * @since 3.6.0 + */ + implementationProvider?: boolean | ImplementationOptions + | ImplementationRegistrationOptions; + + /** + * The server provides find references support. + */ + referencesProvider?: boolean | ReferenceOptions; + +#endif + optional documentHighlightProvider; optional documentSymbolProvider; + optional codeActionProvider; + +#if 0 + /** + * The server provides code lens. + */ + codeLensProvider?: CodeLensOptions; + + /** + * The server provides document link support. + */ + documentLinkProvider?: DocumentLinkOptions; + + /** + * The server provides color provider support. + * + * @since 3.6.0 + */ + colorProvider?: boolean | DocumentColorOptions + | DocumentColorRegistrationOptions; + + /** + * The server provides document formatting. + */ + documentFormattingProvider?: boolean | DocumentFormattingOptions; + + /** + * The server provides document range formatting. + */ + documentRangeFormattingProvider?: boolean | DocumentRangeFormattingOptions; + + /** + * The server provides document formatting on typing. + */ + documentOnTypeFormattingProvider?: DocumentOnTypeFormattingOptions; + + /** + * The server provides rename support. RenameOptions may only be + * specified if the client states that it supports + * `prepareSupport` in its initial `initialize` request. + */ + renameProvider?: boolean | RenameOptions; + + /** + * The server provides folding provider support. + * + * @since 3.10.0 + */ + foldingRangeProvider?: boolean | FoldingRangeOptions + | FoldingRangeRegistrationOptions; + + /** + * The server provides execute command support. + */ + executeCommandProvider?: ExecuteCommandOptions; + + /** + * The server provides selection range support. + * + * @since 3.15.0 + */ + selectionRangeProvider?: boolean | SelectionRangeOptions + | SelectionRangeRegistrationOptions; + + /** + * The server provides linked editing range support. + * + * @since 3.16.0 + */ + linkedEditingRangeProvider?: boolean | LinkedEditingRangeOptions + | LinkedEditingRangeRegistrationOptions; + + /** + * The server provides call hierarchy support. + * + * @since 3.16.0 + */ + callHierarchyProvider?: boolean | CallHierarchyOptions + | CallHierarchyRegistrationOptions; +#endif + optional semanticTokensProvider; + +#if 0 + /** + * Whether server provides moniker support. + * + * @since 3.16.0 + */ + monikerProvider?: boolean | MonikerOptions | MonikerRegistrationOptions; + + /** + * The server provides type hierarchy support. + * + * @since 3.17.0 + */ + typeHierarchyProvider?: boolean | TypeHierarchyOptions + | TypeHierarchyRegistrationOptions; + + /** + * The server provides inline values. + * + * @since 3.17.0 + */ + inlineValueProvider?: boolean | InlineValueOptions + | InlineValueRegistrationOptions; + + /** + * The server provides inlay hints. + * + * @since 3.17.0 + */ + inlayHintProvider?: boolean | InlayHintOptions + | InlayHintRegistrationOptions; + + /** + * The server has support for pull model diagnostics. + * + * @since 3.17.0 + */ + diagnosticProvider?: DiagnosticOptions | DiagnosticRegistrationOptions; + + /** + * The server provides workspace symbol support. + */ + workspaceSymbolProvider?: boolean | WorkspaceSymbolOptions; + + /** + * Workspace specific server capabilities + */ + workspace?: { + /** + * The server supports workspace folder. + * + * @since 3.6.0 + */ + workspaceFolders?: WorkspaceFoldersServerCapabilities; + + /** + * The server is interested in file notifications/requests. + * + * @since 3.16.0 + */ + fileOperations?: { + /** + * The server is interested in receiving didCreateFiles + * notifications. + */ + didCreate?: FileOperationRegistrationOptions; + + /** + * The server is interested in receiving willCreateFiles requests. + */ + willCreate?: FileOperationRegistrationOptions; + + /** + * The server is interested in receiving didRenameFiles + * notifications. + */ + didRename?: FileOperationRegistrationOptions; + + /** + * The server is interested in receiving willRenameFiles requests. + */ + willRename?: FileOperationRegistrationOptions; + + /** + * The server is interested in receiving didDeleteFiles file + * notifications. + */ + didDelete?: FileOperationRegistrationOptions; + + /** + * The server is interested in receiving willDeleteFiles file + * requests. + */ + willDelete?: FileOperationRegistrationOptions; + }; + }; + + /** + * Experimental server capabilities. + */ + experimental?: LSPAny; + #endif }; struct InitializeResult @@ -346,6 +722,9 @@ struct Position { DECLARE_JSON_METHODS(Position) + static Position + from_location_t (location_t); + static Position from_expanded_location (const expanded_location &); @@ -360,6 +739,12 @@ struct Range static Range from_location_t (location_t); + static Range + from_location_t_pair (location_t start, location_t finish); + + bool + contains_p (const Position &p) const; + Position start; Position end; }; @@ -445,7 +830,7 @@ struct TextDocumentPositionParams struct DiagnosticSeverity { DECLARE_JSON_METHODS(DiagnosticSeverity) - enum { + enum class values { Error = 1, Warning = 2, Information = 3, @@ -495,26 +880,310 @@ struct Diagnostic #endif optional> relatedInformation; + optional data; +}; + +struct PublishDiagnosticsParams +{ + DECLARE_JSON_METHODS(PublishDiagnosticsParams) + + DocumentUri uri; + optional version; + vector diagnostics; +}; + +struct CompletionParams : public TextDocumentPositionParams +{ + DECLARE_JSON_METHODS(CompletionParams) + + // FIXME +}; + +struct CompletionItemKind +{ + DECLARE_JSON_METHODS(CompletionItemKind) + + enum class values { + Text = 1, + Method = 2, + Function = 3, + Constructor = 4, + Field = 5, + Variable = 6, + Class = 7, + Interface = 8, + Module = 9, + Property = 10, + Unit = 11, + Value = 12, + Enum = 13, + Keyword = 14, + Snippet = 15, + Color = 16, + File = 17, + Reference = 18, + Folder = 19, + EnumMember = 20, + Constant = 21, + Struct = 22, + Event = 23, + Operator = 24, + TypeParameter = 25 + } m_val; +}; +struct CompletionItem +{ + DECLARE_JSON_METHODS(CompletionItem) + string label; #if 0 /** - * A data entry field that is preserved between a - * `textDocument/publishDiagnostics` notification and - * `textDocument/codeAction` request. + * Additional details for the label + * + * @since 3.17.0 + */ + labelDetails?: CompletionItemLabelDetails; +#endif + + optional kind; + +#if 0 + /** + * Tags for this completion item. + * + * @since 3.15.0 + */ + tags?: CompletionItemTag[]; + + /** + * A human-readable string with additional information + * about this item, like type or symbol information. + */ + detail?: string; + + /** + * A human-readable string that represents a doc-comment. + */ + documentation?: string | MarkupContent; + + /** + * Indicates if this item is deprecated. + * + * @deprecated Use `tags` instead if supported. + */ + deprecated?: boolean; + + /** + * Select this item when showing. + * + * *Note* that only one completion item can be selected and that the + * tool / client decides which item that is. The rule is that the *first* + * item of those that match best is selected. + */ + preselect?: boolean; + + /** + * A string that should be used when comparing this item + * with other items. When omitted the label is used + * as the sort text for this item. + */ + sortText?: string; + + /** + * A string that should be used when filtering a set of + * completion items. When omitted the label is used as the + * filter text for this item. + */ + filterText?: string; + + /** + * A string that should be inserted into a document when selecting + * this completion. When omitted the label is used as the insert text + * for this item. + * + * The `insertText` is subject to interpretation by the client side. + * Some tools might not take the string literally. For example + * VS Code when code complete is requested in this example + * `con` and a completion item with an `insertText` of + * `console` is provided it will only insert `sole`. Therefore it is + * recommended to use `textEdit` instead since it avoids additional client + * side interpretation. + */ + insertText?: string; + + /** + * The format of the insert text. The format applies to both the + * `insertText` property and the `newText` property of a provided + * `textEdit`. If omitted defaults to `InsertTextFormat.PlainText`. + * + * Please note that the insertTextFormat doesn't apply to + * `additionalTextEdits`. + */ + insertTextFormat?: InsertTextFormat; + + /** + * How whitespace and indentation is handled during completion + * item insertion. If not provided the client's default value depends on + * the `textDocument.completion.insertTextMode` client capability. * * @since 3.16.0 + * @since 3.17.0 - support for `textDocument.completion.insertTextMode` + */ + insertTextMode?: InsertTextMode; + + /** + * An edit which is applied to a document when selecting this completion. + * When an edit is provided the value of `insertText` is ignored. + * + * *Note:* The range of the edit must be a single line range and it must + * contain the position at which completion has been requested. + * + * Most editors support two different operations when accepting a completion + * item. One is to insert a completion text and the other is to replace an + * existing text with a completion text. Since this can usually not be + * predetermined by a server it can report both ranges. Clients need to + * signal support for `InsertReplaceEdit`s via the + * `textDocument.completion.completionItem.insertReplaceSupport` client + * capability property. + * + * *Note 1:* The text edit's range as well as both ranges from an insert + * replace edit must be a [single line] and they must contain the position + * at which completion has been requested. + * *Note 2:* If an `InsertReplaceEdit` is returned the edit's insert range + * must be a prefix of the edit's replace range, that means it must be + * contained and starting at the same position. + * + * @since 3.16.0 additional type `InsertReplaceEdit` + */ + textEdit?: TextEdit | InsertReplaceEdit; + + /** + * The edit text used if the completion item is part of a CompletionList and + * CompletionList defines an item default for the text edit range. + * + * Clients will only honor this property if they opt into completion list + * item defaults using the capability `completionList.itemDefaults`. + * + * If not provided and a list's default range is provided the label + * property is used as a text. + * + * @since 3.17.0 + */ + textEditText?: string; + + /** + * An optional array of additional text edits that are applied when + * selecting this completion. Edits must not overlap (including the same + * insert position) with the main edit nor with themselves. + * + * Additional text edits should be used to change text unrelated to the + * current cursor position (for example adding an import statement at the + * top of the file if the completion item will insert an unqualified type). + */ + additionalTextEdits?: TextEdit[]; + + /** + * An optional set of characters that when pressed while this completion is + * active will accept it first and then type that character. *Note* that all + * commit characters should have `length=1` and that superfluous characters + * will be ignored. + */ + commitCharacters?: string[]; + + /** + * An optional command that is executed *after* inserting this completion. + * *Note* that additional modifications to the current document should be + * described with the additionalTextEdits-property. + */ + command?: Command; + + /** + * A data entry field that is preserved on a completion item between + * a completion and a completion resolve request. */ data?: LSPAny; #endif }; -struct PublishDiagnosticsParams +struct CompletionList { - DECLARE_JSON_METHODS(PublishDiagnosticsParams) + DECLARE_JSON_METHODS(CompletionList) +#if 0 + /** + * This list is not complete. Further typing should result in recomputing + * this list. + * + * Recomputed lists have all their items replaced (not appended) in the + * incomplete completion sessions. + */ + isIncomplete: boolean; - DocumentUri uri; - optional version; - vector diagnostics; + /** + * In many cases the items of an actual completion result share the same + * value for properties like `commitCharacters` or the range of a text + * edit. A completion list can therefore define item defaults which will + * be used if a completion item itself doesn't specify the value. + * + * If a completion list specifies a default value and a completion item + * also specifies a corresponding value the one from the item is used. + * + * Servers are only allowed to return default values if the client + * signals support for this via the `completionList.itemDefaults` + * capability. + * + * @since 3.17.0 + */ + itemDefaults?: { + /** + * A default commit character set. + * + * @since 3.17.0 + */ + commitCharacters?: string[]; + + /** + * A default edit range + * + * @since 3.17.0 + */ + editRange?: Range | { + insert: Range; + replace: Range; + }; + + /** + * A default insert text format + * + * @since 3.17.0 + */ + insertTextFormat?: InsertTextFormat; + + /** + * A default insert text mode + * + * @since 3.17.0 + */ + insertTextMode?: InsertTextMode; + + /** + * A default data value. + * + * @since 3.17.0 + */ + data?: LSPAny; + } +#endif + vector items; +}; + +struct DeclarationParams : public TextDocumentPositionParams +{ + DECLARE_JSON_METHODS(DeclarationParams) +}; + +struct DefinitionParams : public TextDocumentPositionParams +{ + DECLARE_JSON_METHODS(DefinitionParams) }; struct DocumentSymbolParams // FIXME @@ -585,7 +1254,208 @@ struct DocumentSymbol optional> children; }; +struct MarkupContent +{ + DECLARE_JSON_METHODS(MarkupContent) + + // FIXME: + // MarkupKind kind; + string kind; + + string value; +}; + +using CodeActionKind = string; + +struct CodeActionTriggerKind +{ + DECLARE_JSON_METHODS(CodeActionTriggerKind) + + enum class values { + Invoked = 1, + Automatic = 2 + } m_val; +}; + +struct CodeActionContext +{ + DECLARE_JSON_METHODS(CodeActionContext) + + vector diagnostics; + optional> only; + optional triggerKind; +}; + +struct CodeActionParams +{ + DECLARE_JSON_METHODS(CodeActionParams) + + TextDocumentIdentifier textDocument; + Range range; + CodeActionContext context; +}; + +struct TextEdit +{ + DECLARE_JSON_METHODS(TextEdit) + + Range range; + string newText; +}; + +using map_DocumentUri_to_vector_TextEdit = map>; + +struct WorkspaceEdit +{ + DECLARE_JSON_METHODS(WorkspaceEdit) + + optional changes; + + // FIXME +}; + +struct CodeAction +{ + DECLARE_JSON_METHODS(CodeAction) + + string title; + optional kind; + optional> diagnostics; + optional isPreferred; + +#if 0 + /** + * Marks that the code action cannot currently be applied. + * + * Clients should follow the following guidelines regarding disabled code + * actions: + * + * - Disabled code actions are not shown in automatic lightbulbs code + * action menus. + * + * - Disabled actions are shown as faded out in the code action menu when + * the user request a more specific type of code action, such as + * refactorings. + * + * - If the user has a keybinding that auto applies a code action and only + * a disabled code actions are returned, the client should show the user + * an error message with `reason` in the editor. + * + * @since 3.16.0 + */ + disabled?: { + + /** + * Human readable description of why the code action is currently + * disabled. + * + * This is displayed in the code actions UI. + */ + reason: string; + }; +#endif + + optional edit; + +#if 0 + optional command; +#endif + + optional data; +}; + +struct HoverParams : public TextDocumentPositionParams +{ + DECLARE_JSON_METHODS(HoverParams) +}; + +struct Hover +{ + DECLARE_JSON_METHODS(Hover) + + MarkupContent contents; + optional range; +}; + +struct DocumentHighlightParams : public TextDocumentPositionParams +{ + DECLARE_JSON_METHODS(DocumentHighlightParams) +}; + +struct DocumentHighlightKind +{ + DECLARE_JSON_METHODS(DocumentHighlightKind) + enum class values { + Text = 1, + Read = 2, + Write = 3 + } m_val; +}; + +struct DocumentHighlight +{ + DECLARE_JSON_METHODS(DocumentHighlight) + + Range range; + optional kind; +}; + +struct SignatureHelpParams : public TextDocumentPositionParams +{ + DECLARE_JSON_METHODS(SignatureHelpParams) + + // FIXME: + // context?: SignatureHelpContext; +}; + +struct ParameterInformation +{ + DECLARE_JSON_METHODS(ParameterInformation) + + string label; + optional documentation; +}; + +struct SignatureInformation +{ + DECLARE_JSON_METHODS(SignatureInformation) + + string label; + optional documentation; + optional> parameters; + optional activeParameter; +}; + +struct SignatureHelp +{ + DECLARE_JSON_METHODS(SignatureHelp) + vector signatures; + optional activeSignature; + optional activeParameter; +}; + } // namespace lsp::spec + +/* A pp_element subclass for printing lsp::spec types, by printing + their JSON serialization. */ + +template +class pp_element : public ::pp_element +{ +public: + pp_element (const T &val) : m_val (val) {} + + void + add_to_phase_2 (pp_markup::context &ctxt) final override + { + auto jv = m_val.to_json (); + jv->print (&ctxt.m_pp, false); + } + +private: + T m_val; +}; + } // namespace lsp #endif /* GCC_LSP_SPEC_H */ diff --git a/gcc/testsuite/gcc.dg/lsp/basics.c b/gcc/testsuite/gcc.dg/lsp/basics.c new file mode 100644 index 00000000000..968ea895719 --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/basics.c @@ -0,0 +1 @@ +/* { dg-final { run-lsp-pytest "basics.py" } } */ diff --git a/gcc/testsuite/gcc.dg/lsp/basics.py b/gcc/testsuite/gcc.dg/lsp/basics.py new file mode 100644 index 00000000000..759122f0ee4 --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/basics.py @@ -0,0 +1,27 @@ +from lsp import * + +import pytest + +def test_unknown_method(): + with GccLspClient(init=False) as c: + c.queue_message(JsonRpcRequest('this-is-not-a-valid-method', {}, id_=0)) + resp = c.serve_until_response(id_=0) + assert resp['jsonrpc'] == '2.0' + assert resp['error']['code'] == METHOD_NOT_FOUND + assert resp['error']['message'] == 'method not found: this-is-not-a-valid-method' + +def test_missing_params(): + """Test missing params in a json-rpc request""" + with GccLspClient(init=False) as c: + id_ = c.queue_message(JsonRpcRequest('initialize', {}, id_=0)) + resp = c.serve_until_response(id_=0) + assert resp['jsonrpc'] == '2.0' + assert resp['error']['code'] == INVALID_PARAMS + assert resp['error']['message'] == "missing 'capabilities' property for InitializeParams object '/params'" + # FIXME: quotes in msg + +def test_initialization(): + with GccLspClient() as c: + assert c.init_response['serverInfo']['name'] == 'GNU C23' + assert type(c.init_response['serverInfo']['version']) == str + assert 'capabilities' in c.init_response diff --git a/gcc/testsuite/gcc.dg/lsp/cli-client.py b/gcc/testsuite/gcc.dg/lsp/cli-client.py deleted file mode 100644 index 447aa9a708a..00000000000 --- a/gcc/testsuite/gcc.dg/lsp/cli-client.py +++ /dev/null @@ -1,270 +0,0 @@ -import json -import os -import re -import select -import subprocess -import sys -import time - -import lsp -from lsp import log - -class ByteConnection: - def __init__(self, fd_receive, fd_send): - self.fd_receive = fd_receive - self.fd_send = fd_send - self.outgoing_data = bytes() - self.incoming_byte_handler = None - - def __repr__(self): - return f'ByteConnection({self.fd_receive}, {self.fd_send}, {self.outgoing_data}, {self.incoming_data})' - - @staticmethod - def from_subprocess(proc): - return ByteConnection(fd_receive = proc.stdout.fileno(), - fd_send = proc.stdin.fileno()) - - def push_data(self, data: bytes): - log('ByteConnection.push_data', repr(data)) - self.outgoing_data += data - - def pop_data(self, data: bytes): - result = self.outgoing_buffers[0] - self.outgoing_buffers = self.outgoing_buffers[1:] - return result - - def blocking_read(self): - received = os.read(self.fd_receive, 4096) - log('ByteConnection.on_receive', f'{len(received)} bytes read from fd {self.fd_receive}') - log('ByteConnection.on_receive', repr(received)) - if self.incoming_byte_handler: - self.incoming_byte_handler(received) - - def blocking_write(self): - if self.outgoing_data: - bytes_written = os.write(self.fd_send, self.outgoing_data) - self.outgoing_data = self.outgoing_data[bytes_written:] - log('ByteConnection.send', f'{bytes_written} bytes written to fd {self.fd_send}') - -class HttpMessage: - def __init__(self, content: bytes): - self.content = content - - def __repr__(self): - return f'HttpMessage({self.content})' - - def to_bytes(self) -> bytes: - return f'Content-Length: {len(self.content)}\r\n\r\n' + self.content - -class HttpParser: - def __init__(self): - self.phase = 'headers' - self.pending_data = bytes() - self.content_length = None - - def consume_octet(self, octet, on_http_message): - #log('HttpParser.consume_octet', repr(octet)) - self.pending_data += octet.to_bytes(length=1, byteorder='big') - #print(f'{self.pending_data=}') - if self.phase == 'headers': - if self.pending_data.endswith(b'\r\n'): - if self.pending_data == b'\r\n': - # blank line; end of headers - #log('HttpParser.consume_octet', 'blank line: end of headers') - self.pending_data = bytes() - self.phase = 'body' - else: - # Header (or initial verb line) - self.parse_header(self.pending_data[:-2]) - self.pending_data = bytes() - else: - assert self.phase == 'body' - assert self.content_length is not None - if len(self.pending_data) == self.content_length: - msg = HttpMessage(content=self.pending_data) - self.phase = 'headers' - self.pending_data = bytes() - self.content_length = None - if on_http_message: - on_http_message(msg) - - def parse_header(self, line: bytes): - # header-field = field-name ":" OWS field-value OWS - log('HttpParser.parse_header', repr(line)) - m = re.match(b'Content-Length:\s+([0-9]+)', line) - if m: - self.content_length = int(m.group(1)) - # FIXME: discard other headers for now - -class HttpConnection: - def __init__(self, byte_conn: ByteConnection): - self.byte_conn = byte_conn - self.outgoing_messages = [] - self.parser = HttpParser() - self.incoming_http_message_handler = None - - def push_message (self, msg: HttpMessage): - #log('HttpConnection.push_message', repr(msg)) - self.byte_conn.push_data(msg.to_bytes().encode('utf-8')) - - def on_incoming_bytes(self, data: bytes): - #log('HttpConnection.on_incoming_bytes', repr(data)) - for b in data: - self.parser.consume_octet(b, self.incoming_http_message_handler) - -class Selector: - def __init__(self): - self.byte_connections = set() - self.poll = select.poll() - self.fd_to_connection = {} - - def add_byte_connection(self, conn: ByteConnection): - self.byte_connections.add(conn) - self.poll.register(conn.fd_receive) - self.poll.register(conn.fd_send) - self.fd_to_connection[conn.fd_receive] = conn - self.fd_to_connection[conn.fd_send] = conn - - def serve_forever(self): - log('Selector.serve_forever', '') - while 1: - res = self.poll.poll() - for fd, event in res: - # print(f'{fd=} {event=}') - if event & select.POLLIN: - print('POLLIN') - conn = self.fd_to_connection[fd] - #print(f'{conn=}') - conn.blocking_read() - if event & select.POLLOUT: - #print('POLLOUT') - conn = self.fd_to_connection[fd] - #print(f'{conn=}') - conn.blocking_write() - -class LspMessage: - def __init__(self, method: str, params, id=None): - self.method = method - self.params = params - self.id = id - - def __repr__(self): - return f'LspMessage({self.method:s}, {self.params}, {self.id})' - - def to_json(self): - res = {"jsonrpc": "2.0", - "method": self.method, - "params": self.params} - if self.id is not None: - res['id'] = self.id - return res - - ''' - @staticmethod - def from_json_request(js_obj): - return LspMessage(method=js_obj['method'], - params=js_obj['params'], - id=js_obj.get('id')) - ''' - -class LspClient: - def __init__(self, http: HttpConnection): - self.http = http - self.next_id = 0 - # map from id to on_response callbacks - self.pending_requests = {} - - # Start initialization handshake - self.send_request('initialize', - lsp.InitializeParams(processId=os.getpid()), - on_response=self.on_initialized) - - def send_request(self, method_name: str, params, on_response): - log('LspClient.send_request', method_name) - lsp_msg = LspMessage(method_name, params.to_json(), self.next_id) - self.next_id += 1 - self.pending_requests[lsp_msg.id] = on_response - self._queue_message(lsp_msg) - - def send_notification(self, method_name: str, params): - log('LspClient.send_notification', method_name) - lsp_msg = LspMessage(method_name, params.to_json()) - self._queue_message(lsp_msg) - - def _queue_message(self, msg: LspMessage): - log('LspClient.queue_message', msg) - js_msg = json.dumps(msg.to_json()) - self.http.push_message(HttpMessage(content=js_msg)) - - def on_incoming_http_message(self, http_msg: HttpMessage): - #log('LspClient.on_incoming_http_message', repr(msg)) - js_msg = json.loads(http_msg.content.decode('utf-8')) - try: - print(f'{js_msg=}') - id_ = js_msg.get('id') - if id_ is None: - # notification - self.on_notification(js_msg) - else: - # response - if id_ in self.pending_requests: - pending_req = self.pending_requests.pop(id_) - if pending_req: - # Could be a success or an error: - pending_req(js_msg) - else: - raise Error(f'unexpected response id: {id_=}' ) - except: - raise foo - - def on_initialized(self, js_msg): - log('LspClient.on_initialized', repr(js_msg)) - - with open(FILENAME) as f: - text = f.read() - - self.send_notification('textDocument/didOpen', - lsp.DidOpenTextDocumentParams(lsp.TextDocumentItem(uri=URI, - languageId='c', # FIXME - version=0, - text=text))) - self.send_request('textDocument/definition', - lsp.TextDocumentPositionParams(textDocument=lsp.TextDocumentIdentifier(URI), - position=lsp.Position(line=8, character=19)), - on_response=self.on_definition) - - def on_definition(self, js_msg): - log('LspClient.on_definition', repr(js_msg)) - - def on_notification(self, js_msg): - log('LspClient.on_notification', repr(js_msg)) - -FILENAME = 'gcc/testsuite/gcc.dg/lsp/test.c' -URI = 'file://' + os.path.abspath(FILENAME) - -if 0: - args=['clangd', '--log=verbose'] -else: - args = ['/home/david/coding-3/gcc-newgit-lsp/build/gcc/cc1', - '-quiet', - '-flsp=0', - '/home/david/coding-3/gcc-newgit-lsp/src/gcc/testsuite/gcc.dg/lsp/test.c'] - -proc = subprocess.Popen(args, - bufsize=0, # unbuffered - stdin=subprocess.PIPE, - stdout=subprocess.PIPE) - -byte_conn = ByteConnection.from_subprocess(proc) - -http_conn = HttpConnection(byte_conn) - -byte_conn.incoming_byte_handler = http_conn.on_incoming_bytes - -client = LspClient(http_conn) - -http_conn.incoming_http_message_handler = client.on_incoming_http_message - -s = Selector() -s.add_byte_connection(byte_conn) -s.serve_forever() diff --git a/gcc/testsuite/gcc.dg/lsp/codeAction-1.c b/gcc/testsuite/gcc.dg/lsp/codeAction-1.c new file mode 100644 index 00000000000..de7daaa5101 --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/codeAction-1.c @@ -0,0 +1,14 @@ +void +test_bad_format_string_args (const char *foo) +{ + __builtin_printf("hello %i", foo); +} + +void +test_misspelled_builtin (const char *foo) +{ + __builtit_printf("hello %s", foo); +} + +/* { dg-excess-errors "" } */ +/* { dg-final { run-lsp-pytest "codeAction-1.py" } } */ diff --git a/gcc/testsuite/gcc.dg/lsp/codeAction-1.py b/gcc/testsuite/gcc.dg/lsp/codeAction-1.py new file mode 100644 index 00000000000..4739144a4f3 --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/codeAction-1.py @@ -0,0 +1,35 @@ +from lsp import * + +import pytest + +C_FILENAME = os.environ['TESTCASE'] + +def test_server_capability(): + with GccLspClient() as c: + assert c.init_response['capabilities']["codeActionProvider"] is True + +def test_FIXME(): + with GccLspClient() as c: + uri = c.open_file(C_FILENAME) + id_ = c.send_request('textDocument/codeAction', + CodeActionParams(textDocument=TextDocumentIdentifier(uri=uri), + position=Position(line=4, + character=2))) + resp = c.serve_until_response(id_) + result = resp['result'] + assert len(result) == 4 + assert result[0]['kind'] == DocumentHighlightKind.Text + assert result[0]['range']['start']['line'] == 0 + assert result[0]['range']['start']['character'] == 12 + assert result[0]['range']['end']['line'] == 0 + assert result[0]['range']['end']['character'] == 18 + + assert result[1]['kind'] == DocumentHighlightKind.Text + assert result[1]['range']['start']['line'] == 4 + assert result[1]['range']['start']['character'] == 2 + assert result[1]['range']['end']['line'] == 4 + assert result[1]['range']['end']['character'] == 10 # FIXME: should be 8 + + # FIXME: etc + +# FIXME: get line numbers from dg-line directives diff --git a/gcc/testsuite/gcc.dg/lsp/completion-1.c b/gcc/testsuite/gcc.dg/lsp/completion-1.c new file mode 100644 index 00000000000..6f1bde21ea8 --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/completion-1.c @@ -0,0 +1,16 @@ +/* */ + +struct foo +{ + int color; + int shape; +}; + +int test (struct foo *ptr) +{ + ptr-> +} + +/* { dg-excess-errors "" } */ +/* FIXME: completion-1.py + Try completion at the "->" */ diff --git a/gcc/testsuite/gcc.dg/lsp/declaration-1.c b/gcc/testsuite/gcc.dg/lsp/declaration-1.c new file mode 100644 index 00000000000..65f16c5ce8d --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/declaration-1.c @@ -0,0 +1,10 @@ +extern void my_fun (void); + +void test (void) +{ + my_fun (); + my_fun (); + my_fun (); +}; + +/* { dg-final { run-lsp-pytest "declaration-1.py" } } */ diff --git a/gcc/testsuite/gcc.dg/lsp/declaration-1.py b/gcc/testsuite/gcc.dg/lsp/declaration-1.py new file mode 100644 index 00000000000..39bf37580e8 --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/declaration-1.py @@ -0,0 +1,37 @@ +from lsp import * + +import pytest + +C_FILENAME = os.environ['TESTCASE'] + +def test_server_capability(): + with GccLspClient() as c: + assert 'declarationProvider' in c.init_response['capabilities'] + +def test_declaration_on_fn_decl(): + with GccLspClient() as c: + uri = c.open_file(C_FILENAME) + id_ = c.send_request('textDocument/declaration', + DeclarationParams(textDocument=TextDocumentIdentifier(uri=uri), + position=Position(line=0, + character=12))) + resp = c.serve_until_response(id_) + result = resp['result'] + assert result is None + +def test_declaration_on_fn_usage(): + with GccLspClient() as c: + uri = c.open_file(C_FILENAME) + id_ = c.send_request('textDocument/declaration', + DeclarationParams(textDocument=TextDocumentIdentifier(uri=uri), + position=Position(line=4, + character=2))) + resp = c.serve_until_response(id_) + result = resp['result'] + assert result['uri'] == uri + assert result['range']['start']['line'] == 0 + assert result['range']['start']['character'] == 12 + assert result['range']['end']['line'] == 0 + assert result['range']['end']['character'] == 18 + +# FIXME: get line numbers from dg-line directives diff --git a/gcc/testsuite/gcc.dg/lsp/definition-1.c b/gcc/testsuite/gcc.dg/lsp/definition-1.c new file mode 100644 index 00000000000..4935570b96a --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/definition-1.c @@ -0,0 +1,10 @@ +void my_fun (void) +{ +} + +void test (void) +{ + my_fun (); +}; + +/* { dg-final { run-lsp-pytest "definition-1.py" } } */ diff --git a/gcc/testsuite/gcc.dg/lsp/definition-1.py b/gcc/testsuite/gcc.dg/lsp/definition-1.py new file mode 100644 index 00000000000..e057ec7a81e --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/definition-1.py @@ -0,0 +1,27 @@ +from lsp import * + +import pytest + +C_FILENAME = os.environ['TESTCASE'] + +def test_server_capability(): + with GccLspClient() as c: + assert 'definitionProvider' in c.init_response['capabilities'] + +def test_definition_on_fn_usage(): + with GccLspClient() as c: + uri = c.open_file(C_FILENAME) + id_ = c.send_request('textDocument/definition', + DefinitionParams(textDocument=TextDocumentIdentifier(uri=uri), + position=Position(line=6, + character=2))) + resp = c.serve_until_response(id_) + result = resp['result'] + assert result['uri'] == uri + # FIXME: this is the opening brace of the defn + assert result['range']['start']['line'] == 1 + assert result['range']['start']['character'] == 0 + assert result['range']['end']['line'] == 1 + assert result['range']['end']['character'] == 1 + +# FIXME: get line numbers from dg-line directives diff --git a/gcc/testsuite/gcc.dg/lsp/documentHighlight-1.c b/gcc/testsuite/gcc.dg/lsp/documentHighlight-1.c new file mode 100644 index 00000000000..88b26e63d9a --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/documentHighlight-1.c @@ -0,0 +1,10 @@ +extern void my_fun (void); + +void test (void) +{ + my_fun (); + my_fun (); + my_fun (); +}; + +/* { dg-final { run-lsp-pytest "documentHighlight-1.py" } } */ diff --git a/gcc/testsuite/gcc.dg/lsp/documentHighlight-1.py b/gcc/testsuite/gcc.dg/lsp/documentHighlight-1.py new file mode 100644 index 00000000000..32fd7917b8c --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/documentHighlight-1.py @@ -0,0 +1,58 @@ +from lsp import * + +import pytest + +C_FILENAME = os.environ['TESTCASE'] + +def test_server_capability(): + with GccLspClient() as c: + assert c.init_response['capabilities']["documentHighlightProvider"] is True + +def test_highlight_on_fn_decl(): + with GccLspClient() as c: + uri = c.open_file(C_FILENAME) + id_ = c.send_request('textDocument/documentHighlight', + DocumentHighlightParams(textDocument=TextDocumentIdentifier(uri=uri), + position=Position(line=0, + character=12))) + resp = c.serve_until_response(id_) + result = resp['result'] + assert result is None + # FIXME: should we return a result for this? + +def test_highlight_on_fn_usage(): + with GccLspClient() as c: + uri = c.open_file(C_FILENAME) + id_ = c.send_request('textDocument/documentHighlight', + DocumentHighlightParams(textDocument=TextDocumentIdentifier(uri=uri), + position=Position(line=4, + character=2))) + resp = c.serve_until_response(id_) + result = resp['result'] + assert len(result) == 4 + assert result[0]['kind'] == DocumentHighlightKind.Text + assert result[0]['range']['start']['line'] == 0 + assert result[0]['range']['start']['character'] == 12 + assert result[0]['range']['end']['line'] == 0 + assert result[0]['range']['end']['character'] == 18 + + assert result[1]['kind'] == DocumentHighlightKind.Text + assert result[1]['range']['start']['line'] == 4 + assert result[1]['range']['start']['character'] == 2 + assert result[1]['range']['end']['line'] == 4 + assert result[1]['range']['end']['character'] == 10 # FIXME: should be 8 + + # FIXME: etc + +def test_no_highlight(): + with GccLspClient() as c: + uri = c.open_file(C_FILENAME) + id_ = c.send_request('textDocument/documentHighlight', + DocumentHighlightParams(textDocument=TextDocumentIdentifier(uri=uri), + position=Position(line=0, + character=0))) + resp = c.serve_until_response(id_) + result = resp['result'] + assert result is None + +# FIXME: get line numbers from dg-line directives diff --git a/gcc/testsuite/gcc.dg/lsp/documentSymbol-1.c b/gcc/testsuite/gcc.dg/lsp/documentSymbol-1.c new file mode 100644 index 00000000000..38281d0f710 --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/documentSymbol-1.c @@ -0,0 +1,17 @@ +struct example_struct +{ +}; + +typedef struct example_struct example_typedef; + +extern int example_var_1; + +int example_var_2; + +extern void example_fn_1(void); + +void example_fn_2 (void) +{ +} + +/* { dg-final { run-lsp-pytest "documentSymbol-1.py" } } */ diff --git a/gcc/testsuite/gcc.dg/lsp/documentSymbol-1.py b/gcc/testsuite/gcc.dg/lsp/documentSymbol-1.py new file mode 100644 index 00000000000..abfd65b3129 --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/documentSymbol-1.py @@ -0,0 +1,65 @@ +from lsp import * + +import pytest + +C_FILENAME = os.environ['TESTCASE'] + +def test_server_capability(): + with GccLspClient() as c: + assert c.init_response['capabilities']['documentSymbolProvider'] is True + +def assert_symbol_for_token(element, + expected_name : str, + expected_kind : int, + expected_line : int, + expected_start_char : int, + expected_end_char :int): + assert element["name"] == expected_name + assert element["kind"] == expected_kind + assert element["range"]["start"]["line"] == expected_line + assert element["range"]["start"]["character"] == expected_start_char + assert element["range"]["end"]["line"] == expected_line + assert element["range"]["end"]["character"] == expected_end_char + +def test_get_symbols(): + with GccLspClient() as c: + uri = c.open_file(C_FILENAME) + id_ = c.send_request('textDocument/documentSymbol', + DocumentSymbolParams(textDocument=TextDocumentIdentifier(uri=uri))) + resp = c.serve_until_response(id_) + result = resp['result'] + if 1: + print(result) + + assert_symbol_for_token(result[0], + expected_name="example_struct", + expected_kind=23, + expected_line=0, + expected_start_char=7, + expected_end_char=21) + assert_symbol_for_token(result[1], + expected_name="example_var_1", + expected_kind=13, + expected_line=6, + expected_start_char=11, + expected_end_char=24) + assert_symbol_for_token(result[2], + expected_name="example_var_2", + expected_kind=13, + expected_line=8, + expected_start_char=4, + expected_end_char=17) + assert_symbol_for_token(result[3], + expected_name="example_fn_1", + expected_kind=12, + expected_line=10, + expected_start_char=12, + expected_end_char=24) + assert_symbol_for_token(result[4], + expected_name="example_fn_2", + expected_kind=12, + expected_line=12, + expected_start_char=5, + expected_end_char=17) + +# FIXME: get line numbers from dg-line directives diff --git a/gcc/testsuite/gcc.dg/lsp/hover-1.c b/gcc/testsuite/gcc.dg/lsp/hover-1.c new file mode 100644 index 00000000000..bad30c5b1f4 --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/hover-1.c @@ -0,0 +1,10 @@ +extern void my_fun (int one, const char *two, float three); + +void test (void) +{ + my_fun (1, "two", 3.0f); +}; + +/* { dg-final { run-lsp-pytest "hover-1.py" } } */ + +/* TODO: Try hover requests on my_fun decl and usage, verify result. */ diff --git a/gcc/testsuite/gcc.dg/lsp/hover-1.py b/gcc/testsuite/gcc.dg/lsp/hover-1.py new file mode 100644 index 00000000000..2b9ac7a68dc --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/hover-1.py @@ -0,0 +1,39 @@ +from lsp import * + +import pytest + +C_FILENAME = os.environ['TESTCASE'] + +def test_server_capability(): + with GccLspClient() as c: + assert 'hoverProvider' in c.init_response['capabilities'] + +def test_hover(): + with GccLspClient() as c: + uri = c.open_file(C_FILENAME) + id_ = c.send_request('textDocument/hover', + HoverParams(textDocument=TextDocumentIdentifier(uri=uri), + position=Position(line=4, + character=2))) + resp = c.serve_until_response(id_) + result = resp['result'] + + assert result['contents']['kind'] == 'plaintext' + assert result['contents']['value'] == ( +"""function 'my_fun' + +Returns: void + +Parameters +- int one +- const char * two +- float three + +void my_fun(int one, const char * two, float three) +""") + assert result['range']['start']['line'] == 4 + assert result['range']['start']['character'] == 2 + assert result['range']['end']['line'] == 4 + assert result['range']['end']['character'] == 10 # FIXME: should be 8 + +# FIXME: get line numbers from dg-line directives diff --git a/gcc/testsuite/gcc.dg/lsp/lsp.exp b/gcc/testsuite/gcc.dg/lsp/lsp.exp new file mode 100644 index 00000000000..a44d273ada8 --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/lsp.exp @@ -0,0 +1,31 @@ +# Copyright (C) 2012-2026 Free Software Foundation, Inc. +# +# This file is part of GCC. +# +# GCC is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3, or (at your option) +# any later version. +# +# GCC is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with GCC; see the file COPYING3. If not see +# . + +# GCC testsuite that uses the `dg.exp' driver. + +# Load support procs. +load_lib gcc-dg.exp + +# Initialize `dg'. +dg-init + +# Main loop. +dg-runtest [lsort [glob -nocomplain $srcdir/$subdir/*.c]] "" "" + +# All done. +dg-finish diff --git a/gcc/testsuite/gcc.dg/lsp/lsp.py b/gcc/testsuite/gcc.dg/lsp/lsp.py deleted file mode 100644 index 088a95b335f..00000000000 --- a/gcc/testsuite/gcc.dg/lsp/lsp.py +++ /dev/null @@ -1,239 +0,0 @@ -# A python module for implementing LSP clients - -import json -import os -import subprocess -import sys - -import jsonrpc # pip install json-rpc -import requests - -def log(what, msg): - print('%s: %s' % (what, msg), file=sys.stderr) - -# Various types to model the LSP interface - -class InitializeParams: - def __init__(self, processId: int): - self.processId = processId - - def to_json(self): - return {'processId': self.processId}#, - #'clientInfo': self.clientInfo.to_json () } - -class Position: - def __init__(self, line, character): - self.line = line - self.character = character - - def __repr__(self): - return 'Position(%r, %r)' % (self.line, self.character) - - def __eq__(self, other): - return self.__dict__ == other.__dict__ - - def to_json(self): - return {'line': self.line, 'character': self.character} - - @staticmethod - def from_json(js): - return Position(js['line'], js['character']) - -class Range: - def __init__(self, start, end): - self.start = start - self.end = end - - def __repr__(self): - return 'Range(%r, %r)' % (self.start, self.end) - - def __eq__(self, other): - return self.__dict__ == other.__dict__ - - @staticmethod - def from_json(js): - return Range(Position.from_json(js['start']), - Position.from_json(js['end'])) - -class Location: - def __init__(self, uri, range): - self.uri = uri - self.range = range - - def __repr__(self): - return 'Location(%r, %r)' % (self.uri, self.range) - - def __eq__(self, other): - return self.__dict__ == other.__dict__ - - @staticmethod - def from_json(js): - return Location(js['uri'], Range.from_json(js['range'])) - - def dump(self, msg): - print('%s:%i:%i: %s' % (self.uri, self.range.start.line, - self.range.start.character, msg)) - # FIXME: underline - # linecache uses 1-based line numbers, whereas LSP uses - # 0-based line numbers - import linecache - line = linecache.getline(self.uri, self.range.start.line + 1) - print('line: %r' % line) - -class TextDocumentIdentifier: - def __init__(self, uri): - self.uri = uri - - def __eq__(self, other): - return self.__dict__ == other.__dict__ - - def to_json(self): - return {'uri': self.uri} - -DocumentUri = str - -class TextDocumentItem: - def __init__(self, - uri: DocumentUri, - languageId: str, - version: int, - text: str): - self.uri = uri - self.languageId = languageId - self.version = version - self.text = text - - def to_json(self): - return {"uri" : self.uri, - "languageId" : self.languageId, - "version": self.version, - "text": self.text} - -class DidOpenTextDocumentParams: - def __init__(self, textDocument: TextDocumentItem): - self.textDocument = textDocument - - def to_json(self): - return {"textDocument" : self.textDocument.to_json()} - -class TextDocumentPositionParams: - def __init__(self, - textDocument: TextDocumentIdentifier, - position: Position): - self.textDocument = textDocument - self.position = position - - def __eq__(self, other): - return self.__dict__ == other.__dict__ - - def to_json(self): - return {"textDocument" : self.textDocument.to_json(), - "position" : self.position.to_json()} - - -class Transport: - def post_request(self, data: str): - raise NotImplementedError - def post_notification(self, data: str): - raise NotImplementedError - -class UrlTransport(Transport): - def __init__(self, url): - self.url = url - - def post_request(self, data: str): - headers = {'content-type': 'application/json'} - response = requests.post(self.url, data=data, headers=headers) - return response - -class SubprocessTransport(Transport): - def __init__(self, args): - self.args = args - self.proc = subprocess.Popen(args, - bufsize=0, # unbuffered - stdin=subprocess.PIPE, - stdout=subprocess.PIPE) - - def post_request(self, data: str): - encoded = data.encode('utf-8') - byte_msg = f'Content-Length: {len(encoded)}\r\n\r\n'.encode('utf-8') + encoded - log('toy-ide', 'sending to subprocess: %r' % byte_msg) - self.proc.stdin.write(byte_msg) - self.proc.stdin.flush() - log('toy-ide', 'sent to subprocess') - #self.proc.stdout.flush() - resp = os.read(self.proc.stdout.fileno(), 4096) # FIXME - log('toy-ide', f'received from subprocess: {resp=}') - #headers = {'content-type': 'application/json'} - #response = requests.post(self.url, data=data, headers=headers) - #return response - - def post_notification(self, data: str): - encoded = data.encode('utf-8') - byte_msg = f'Content-Length: {len(encoded)}\r\n\r\n'.encode('utf-8') + encoded - log('toy-ide', 'sending to subprocess: %r' % byte_msg) - self.proc.stdin.write(byte_msg) - self.proc.stdin.flush() - log('toy-ide', 'sent to subprocess') - #resp = self.proc.stdout.read(); - #log('toy-ide', f'received from subprocess: {resp=}') - ''' - headers = {'content-type': 'application/json'} - response = requests.post(self.url, data=data, headers=headers) - return response - ''' - -# A wrapper for making LSP calls against a server - -class Proxy: - def __init__(self, transport: Transport): - self.transport = transport - self.next_id = 0 - - # TODO: initialization - params = InitializeParams() - json_resp = self.post_request('initialize', params.to_json()) - print(json_resp) - - def make_request(self, method, params): - json_req = {"method": method, - "params": params, - "jsonrpc": "2.0", - "id": self.next_id} - self.next_id += 1 - return json_req - - def make_notification(self, method, params): - json_req = {"method": method, - "params": params, - "jsonrpc": "2.0"} - return json_req - - def post_request(self, method, params): - json_obj = self.make_request(method, params) - response = self.transport.post_request(json.dumps(json_obj)) - #print('response: %r' % response) - #print('response.json(): %r' % response.json()) - #return response.json() - - def post_notification(self, method, params): - json_obj = self.make_notification(method, params) - response = self.transport.post_notification(json.dumps(json_obj)) - #print('response: %r' % response) - #print('response.json(): %r' % response.json()) - #return response.json() - - def textDocument_didOpen(self, params: DidOpenTextDocumentParams): - self.post_notification('textDocument/didOpen', - params.to_json()) - - def textDocument_definition(self, params: TextDocumentPositionParams): - json_resp = self.post_request('textDocument/definition', - params.to_json()) - print(f'{json_resp=}') - raise foo - # Expect either a Location or a list of Location - if isinstance(json_resp, list): - return [Location.from_json(item) for item in json_resp] - else: - return Location.from_json(json_resp) diff --git a/gcc/testsuite/gcc.dg/lsp/publishDiagnostics-1.c b/gcc/testsuite/gcc.dg/lsp/publishDiagnostics-1.c new file mode 100644 index 00000000000..e9f9fc8ea73 --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/publishDiagnostics-1.c @@ -0,0 +1,12 @@ +void returns_non_void () +{ + return 42; +} + +void test_bad_format_string_args (const char *foo) +{ + __builtin_printf("hello %i", foo); +} + +/* { dg-excess-errors "" } */ +/* { dg-final { run-lsp-pytest "publishDiagnostics-1.py" } } */ diff --git a/gcc/testsuite/gcc.dg/lsp/publishDiagnostics-1.py b/gcc/testsuite/gcc.dg/lsp/publishDiagnostics-1.py new file mode 100644 index 00000000000..c8f74cea745 --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/publishDiagnostics-1.py @@ -0,0 +1,98 @@ +from lsp import * + +import pytest + +C_FILENAME = os.environ['TESTCASE'] + +def assert_Diagnostic(actual:dict, + expected_range:Range, + expected_severity:int, + expected_desc_suffix:str, + expected_message:str): + assert actual['range'] == expected_range.to_json() + assert actual['severity'] == expected_severity + if expected_desc_suffix: + assert actual['codeDescription']['href'].endswith(expected_desc_suffix) + assert actual['message'] == expected_message + +def assert_DiagnosticRelatedInformation(actual:dict, + expected_location:Location, + expected_message:str): + assert actual['location'] == expected_location.to_json() + assert actual['message'] == expected_message + +def test_diagnostics(): + with GccLspClient() as c: + uri = c.open_file(C_FILENAME) + notif = c.serve_until_notification("textDocument/publishDiagnostics") + params = notif['params'] + if 1: + print(params) + assert params['uri'] == uri + + diag0 = params['diagnostics'][0] + assert_Diagnostic( + diag0, + expected_range=make_Range_for_token( + line=2, + start_character=9, + end_character=11), + expected_severity=1, + expected_desc_suffix='/Warning-Options.html#index-Wno-return-mismatch', + expected_message="'return' with a value, in function returning void") + assert_DiagnosticRelatedInformation( + diag0['relatedInformation'][0], + expected_location=make_Location_for_token( + uri=uri, + line=2, + start_character=9, + end_character=11), + expected_message="FIXME") # FIXME + + diag1 = params['diagnostics'][1] + assert_Diagnostic( + diag1, + expected_range=make_Range_for_token( + line=0, + start_character=5, + end_character=21), + expected_severity=3, + expected_desc_suffix=None, + expected_message="declared here") + # FIXME: this should be relatedInformation within the first diagnostic + + diag2 = params['diagnostics'][2] + assert_Diagnostic( + diag2, + expected_range=make_Range_for_token( + line=7, + start_character=26, + end_character=28), + expected_severity=2, + expected_desc_suffix='/Warning-Options.html#index-Wformat', + expected_message="format '%i' expects argument of type 'int', but argument 2 has type 'const char *'") + assert_DiagnosticRelatedInformation( + diag2['relatedInformation'][0], + expected_location=make_Location_for_token( + uri=uri, + line=7, + start_character=26, + end_character=28), + expected_message="int") + assert_DiagnosticRelatedInformation( + diag2['relatedInformation'][1], + expected_location=make_Location_for_token( + uri=uri, + line=7, + start_character=31, + end_character=34), + expected_message="const char *") + + # We expect data for the codeAction to be stashed in 'data': + assert diag2['data']['title'] == "Replace '%i' with '%s'" + assert diag2['data']["kind"] == "quickfix" + assert diag2['data']["isPreferred"] == True + assert diag2['data']["edit"]["changes"] == {uri: [{"range": make_Range_for_token(7, 26, 28).to_json(), + "newText": "%s"}]} + +# FIXME: get line numbers from dg-line directives diff --git a/gcc/testsuite/gcc.dg/lsp/signatureHelp-1.c b/gcc/testsuite/gcc.dg/lsp/signatureHelp-1.c new file mode 100644 index 00000000000..510ebf0f0a3 --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/signatureHelp-1.c @@ -0,0 +1,9 @@ +extern void my_fun (int one, const char *two, float three); + +void test_opening_paren_of_call (void) +{ + my_fun( +}; + +/* { dg-excess-errors "" } */ +/* { dg-final { run-lsp-pytest "signatureHelp-1.py" } } */ diff --git a/gcc/testsuite/gcc.dg/lsp/signatureHelp-1.py b/gcc/testsuite/gcc.dg/lsp/signatureHelp-1.py new file mode 100644 index 00000000000..cd687998611 --- /dev/null +++ b/gcc/testsuite/gcc.dg/lsp/signatureHelp-1.py @@ -0,0 +1,32 @@ +from lsp import * + +import pytest + +C_FILENAME = os.environ['TESTCASE'] + +def test_server_capability(): + with GccLspClient() as c: + assert c.init_response['capabilities']['signatureHelpProvider']['triggerCharacters'] == ["(", ","] + +def test_opening_paren_of_call(): + with GccLspClient() as c: + uri = c.open_file(C_FILENAME) + id_ = c.send_request('textDocument/signatureHelp', + SignatureHelpParams(textDocument=TextDocumentIdentifier(uri=uri), + position=Position(line=4, + character=9))) + resp = c.serve_until_response(id_) + result = resp['result'] + if 0: + print(result) + + assert len(result['signatures']) == 1 + assert result['signatures'][0]['label'] == "\nvoid my_fun(int one, const char * two, float three)" + assert len(result['signatures'][0]['parameters']) == 3 + assert result['signatures'][0]['parameters'][0]['label'] == "int one" + assert result['signatures'][0]['parameters'][1]['label'] == "const char * two" + assert result['signatures'][0]['parameters'][2]['label'] == "float three" + assert result["activeSignature"] == 0 + assert result["activeParameter"] == 0 + +# FIXME: get line numbers from dg-line directives diff --git a/gcc/testsuite/gcc.dg/lsp/test.c b/gcc/testsuite/gcc.dg/lsp/test.c deleted file mode 100644 index bb80bd06086..00000000000 --- a/gcc/testsuite/gcc.dg/lsp/test.c +++ /dev/null @@ -1,12 +0,0 @@ -/* */ - -struct foo -{ - int color; - int shape; -}; - -int test (struct foo *ptr) -{ - ptr-> -} diff --git a/gcc/testsuite/gcc.dg/lsp/test.py b/gcc/testsuite/gcc.dg/lsp/test.py deleted file mode 100644 index c7f00670079..00000000000 --- a/gcc/testsuite/gcc.dg/lsp/test.py +++ /dev/null @@ -1,28 +0,0 @@ -from lsp import Proxy, TextDocumentIdentifier, Position, Range, Location - -def main(): - # This assumes that we're running this: - # (hardcoding the particular source file for now): - # ./xgcc -B. -c ../../src/gcc/testsuite/gcc.dg/lsp/test.c \ - # -flsp=4000 -fblt -wrapper gdb,--args - - url = "http://localhost:4000/jsonrpc" - proxy = Proxy(url) - - # FIXME: filename/uri (currently a particular relative location) - FILENAME = '../../src/gcc/testsuite/gcc.dg/lsp/test.c' - - # Ask for the location of a usage of "struct foo" (0-based lines) - result = proxy.goto_definition(TextDocumentIdentifier(FILENAME), - Position(8, 16)) - - # We expect to get back the location of where "struct foo" is defined - print(result) - # FIXME: filename/uri (currently a particular relative location) - assert result == [Location(FILENAME, - Range(Position(2, 0), Position(6, 1)))] - for loc in result: - loc.dump("definition") - -if __name__ == "__main__": - main() diff --git a/gcc/testsuite/gcc.dg/lsp/toy-ide.py b/gcc/testsuite/gcc.dg/lsp/toy-ide.py deleted file mode 100644 index d49cf48fceb..00000000000 --- a/gcc/testsuite/gcc.dg/lsp/toy-ide.py +++ /dev/null @@ -1,182 +0,0 @@ -# A toy IDE implemented in PyGTK - -import os -import sys - -import gi - -gi.require_version("Gtk", "3.0") -gi.require_version("GtkSource", "4") -from gi.repository import GLib, Gio, Gtk, GtkSource - -import lsp -from lsp import log - -# This would typically be its own file -MENU_XML = """ - - - -
- - win.goto_definition - Goto Definition - -
-
- - app.about - _About - - - app.quit - _Quit - <Primary>q - -
-
-
-""" - -class ToyIdeWindow(Gtk.ApplicationWindow): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # This will be in the windows group and have the "win" prefix - goto_definition_action = Gio.SimpleAction.new_stateful( - "goto_definition", None, GLib.Variant.new_boolean(False) - ) - goto_definition_action.connect("change-state", self.on_goto_definition) - self.add_action(goto_definition_action) - - self.buf = GtkSource.Buffer() - - self.filename = 'gcc/testsuite/gcc.dg/lsp/test.c' - with open(self.filename) as f: - text = f.read() - self.buf.set_text(text) - - # FIXME: - self.sv = GtkSource.View(buffer=self.buf) - self.sv.set_show_line_numbers(True) - self.add(self.sv) - self.sv.show() - - if 1: - transport = lsp.SubprocessTransport(['clangd', '--log=verbose']) - #transport = lsp.SubprocessTransport(['/home/david/coding-3/gcc-newgit-lsp/build/gcc/cc1', '-quiet, '-flsp=0', '/home/david/coding-3/gcc-newgit-lsp/src/gcc/testsuite/gcc.dg/lsp/test.c']) - #transport = lsp.SubprocessTransport(['cat']) - else: - transport = lsp.UrlTransport('http://localhost:4000/jsonrpc') - self.lsp = lsp.Proxy(transport) - - textDocument = lsp.TextDocumentItem(uri=self.get_uri(), - languageId='c', # FIXME - version=0, - text=text) - params = lsp.DidOpenTextDocumentParams(textDocument) - self.lsp.textDocument_didOpen(params) - - def get_uri(self): - return 'file://' + os.path.abspath(self.filename) - - def on_change_label_state(self, action, value): - action.set_state(value) - self.label.set_text(value.get_string()) - - def get_cursor_position(self): - """Get the position of the cursor within the buffer - as an lsp.Position""" - mark = self.buf.get_insert() - #print(mark) - iter = self.buf.get_iter_at_mark(mark) - #print(iter) - - #print('line: %r' % iter.get_line()) # 0-based line - #print('line_offset: %r' % iter.get_line_offset()) # 0-based offse - - return lsp.Position(iter.get_line(), iter.get_line_offset()) - - def on_goto_definition(self, action, value): - log("ToyIdeWindow", "on_goto_definition") - - # FIXME: need to sort out paths between the LSP server and client - uri = self.get_uri() - params = lsp.TextDocumentPositionParams(textDocument=lsp.TextDocumentIdentifier(uri), - position=self.get_cursor_position()) - locs = self.lsp.textDocument_definition(params) - print(locs) - if len(locs) == 1: - loc = locs[0] - # Both lsp.Range and Gtk.TextBuffer.select_range are inclusive - # on the start, exclusive on the end-point - self.buf.select_range( - self.buf.get_iter_at_line_offset(loc.range.start.line, - loc.range.start.character), - self.buf.get_iter_at_line_offset(loc.range.end.line, - loc.range.end.character)) - -class ToyIdeApp(Gtk.Application): - def __init__(self, *args, **kwargs): - super().__init__( - *args, - application_id="org.example.myapp", - flags=Gio.ApplicationFlags.HANDLES_COMMAND_LINE, - **kwargs - ) - self.window = None - - self.add_main_option( - "test", - ord("t"), - GLib.OptionFlags.NONE, - GLib.OptionArg.NONE, - "Command line test", - None, - ) - - def do_startup(self): - Gtk.Application.do_startup(self) - - action = Gio.SimpleAction.new("about", None) - action.connect("activate", self.on_about) - self.add_action(action) - - action = Gio.SimpleAction.new("quit", None) - action.connect("activate", self.on_quit) - self.add_action(action) - - builder = Gtk.Builder.new_from_string(MENU_XML, -1) - self.set_app_menu(builder.get_object("app-menu")) - - def do_activate(self): - # We only allow a single window and raise any existing ones - if not self.window: - # Windows are associated with the application - # when the last one is closed the application shuts down - self.window = ToyIdeWindow(application=self, title="Main Window") - - self.window.present() - - def do_command_line(self, command_line): - options = command_line.get_options_dict() - # convert GVariantDict -> GVariant -> dict - options = options.end().unpack() - - if "test" in options: - # This is printed on the main instance - print("Test argument recieved: %s" % options["test"]) - - self.activate() - return 0 - - def on_about(self, action, param): - about_dialog = Gtk.AboutDialog(transient_for=self.window, modal=True) - about_dialog.present() - - def on_quit(self, action, param): - self.quit() - -if __name__ == "__main__": - app = ToyIdeApp() - app.run(sys.argv) diff --git a/gcc/testsuite/lib/gcc-dg.exp b/gcc/testsuite/lib/gcc-dg.exp index f2e5228fff5..2f6c6cfacd3 100644 --- a/gcc/testsuite/lib/gcc-dg.exp +++ b/gcc/testsuite/lib/gcc-dg.exp @@ -27,6 +27,7 @@ load_lib scanwpaipa.exp load_lib scanlang.exp load_lib scansarif.exp load_lib scanhtml.exp +load_lib lsp.exp load_lib timeout.exp load_lib timeout-dg.exp load_lib prune.exp diff --git a/gcc/testsuite/lib/lsp.exp b/gcc/testsuite/lib/lsp.exp new file mode 100644 index 00000000000..ce4d60a3d90 --- /dev/null +++ b/gcc/testsuite/lib/lsp.exp @@ -0,0 +1,93 @@ +# Copyright (C) 2000-2026 Free Software Foundation, Inc. + +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with GCC; see the file COPYING3. If not see +# . + +proc lsp-pytest-format-line { args } { + global subdir + + set pytest_script [lindex $args 0] + set output_line [lindex $args 1] + + verbose "output_line: $output_line" 2 + + set index [string first "::" $output_line] + set test_output [string range $output_line [expr $index + 2] [string length $output_line]] + + return "$subdir/${pytest_script} ${test_output}" +} + + +# Call by dg-final to run a pytest Python script. + +proc run-lsp-pytest { args } { + global srcdir subdir + + # Extract the test script file name from the arguments. + set pytest_script [lindex $args 0] + + verbose "LSP: running $pytest_script in $srcdir/$subdir" 2 + set pytest_script [remote_download host $pytest_script] + + if { ![check_effective_target_pytest3] } { + unsupported "$pytest_script pytest python3 is missing" + return + } + + set libdir "${srcdir}/lib" + + # Set/prepend libdir to PYTHONPATH + if [info exists ::env(PYTHONPATH)] { + set old_PYTHONPATH $::env(PYTHONPATH) + setenv PYTHONPATH "${libdir}:${old_PYTHONPATH}" + } else { + setenv PYTHONPATH "${libdir}" + } + + set testcase [testname-for-summary] + verbose "testcase: $testcase" 2 + if [info exists ::env(TESTCASE)] { + set old_TESTCASE $::env(TESTCASE) + } + setenv TESTCASE "$srcdir/$testcase" + + verbose "LANG=C TESTCASE=[getenv TESTCASE] PYTHONPATH=[getenv PYTHONPATH] python3 -m pytest --color=no -rap -s --tb=no $srcdir/$subdir/$pytest_script" 2 + spawn -noecho python3 -m pytest --color=no -rap -s --tb=no $srcdir/$subdir/$pytest_script + + if [info exists old_PYTHONPATH] { + setenv PYTHONPATH ${old_PYTHONPATH} + } + if [info exists old_TESTCASE] { + setenv TESTCASE ${old_TESTCASE} + } + + set prefix "\[^\r\n\]*" + expect { + -re "FAILED($prefix)\[^\r\n\]+\r\n" { + set output [lsp-pytest-format-line $pytest_script $expect_out(1,string)] + fail $output + exp_continue + } + -re "ERROR($prefix)\[^\r\n\]+\r\n" { + set output [lsp-pytest-format-line $pytest_script $expect_out(1,string)] + fail $output + exp_continue + } + -re "PASSED($prefix)\[^\r\n\]+\r\n" { + set output [lsp-pytest-format-line $pytest_script $expect_out(1,string)] + pass $output + exp_continue + } + } +} diff --git a/gcc/testsuite/lib/lsp.py b/gcc/testsuite/lib/lsp.py new file mode 100644 index 00000000000..cc882ea8011 --- /dev/null +++ b/gcc/testsuite/lib/lsp.py @@ -0,0 +1,631 @@ +# FIXME: +# was: A python module for implementing LSP clients +# TODO: turn this into a Python module for writing LSP test cases + +import json +import os +from pathlib import Path +import re +import select +import subprocess +import sys + +# The standard JSON-RPC error codes. +PARSE_ERROR = -32700; +INVALID_REQUEST = -32600; +METHOD_NOT_FOUND = -32601; +INVALID_PARAMS = -32602; +INTERNAL_ERROR = -32603; + +def log(what, msg): + if 0: + print('%s: %s' % (what, msg), file=sys.stderr) + +# Various types to model the LSP interface + +class ClientCapabilities: + def to_json(self): + return {} + +class InitializeParams: + def __init__(self, + processId: int, + capabilities:ClientCapabilities): + self.processId = processId + self.capabilities = capabilities + + def to_json(self): + return {'processId': self.processId, + 'capabilities': self.capabilities.to_json()} + +class InitializedParams: + def to_json(self): + return {} + +class Position: + def __init__(self, line, character): + self.line = line + self.character = character + + def __repr__(self): + return 'Position(%r, %r)' % (self.line, self.character) + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + def to_json(self): + return {'line': self.line, 'character': self.character} + + @staticmethod + def from_json(js): + return Position(js['line'], js['character']) + +class Range: + def __init__(self, start:Position, end:Position): + self.start = start + self.end = end + + def __repr__(self): + return 'Range(%r, %r)' % (self.start, self.end) + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + @staticmethod + def from_json(js): + return Range(Position.from_json(js['start']), + Position.from_json(js['end'])) + + def to_json(self): + return {'start': self.start.to_json (), + 'end': self.end.to_json()} + +class Location: + def __init__(self, + uri: str, + range_: Range): + self.uri = uri + self.range_ = range_ + + def __repr__(self): + return 'Location(%r, %r)' % (self.uri, self.range_) + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + @staticmethod + def from_json(js): + return Location(js['uri'], Range.from_json(js['range'])) + + def to_json(self): + return {'uri': self.uri, + 'range': self.range_.to_json()} + + def dump(self, msg): + print('%s:%i:%i: %s' % (self.uri, self.range_.start.line, + self.range_.start.character, msg)) + # FIXME: underline + # linecache uses 1-based line numbers, whereas LSP uses + # 0-based line numbers + import linecache + line = linecache.getline(self.uri, self.range_.start.line + 1) + print('line: %r' % line) + +class TextDocumentIdentifier: + def __init__(self, uri): + self.uri = uri + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + def to_json(self): + return {'uri': self.uri} + +DocumentUri = str + +class TextDocumentItem: + def __init__(self, + uri: DocumentUri, + languageId: str, + version: int, + text: str): + self.uri = uri + self.languageId = languageId + self.version = version + self.text = text + + def to_json(self): + return {"uri" : self.uri, + "languageId" : self.languageId, + "version": self.version, + "text": self.text} + +class DidOpenTextDocumentParams: + def __init__(self, textDocument: TextDocumentItem): + self.textDocument = textDocument + + def to_json(self): + return {"textDocument" : self.textDocument.to_json()} + +class TextDocumentPositionParams: + def __init__(self, + textDocument: TextDocumentIdentifier, + position: Position): + self.textDocument = textDocument + self.position = position + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + def to_json(self): + return {"textDocument" : self.textDocument.to_json(), + "position" : self.position.to_json()} + +class DeclarationParams(TextDocumentPositionParams): + pass + +class DefinitionParams(TextDocumentPositionParams): + pass + +class DocumentHighlightParams(TextDocumentPositionParams): + pass + +class DocumentHighlightKind: + Text = 1 + Read = 2 + Write = 3 + +class DocumentSymbolParams: + def __init__(self, + textDocument: TextDocumentIdentifier): + self.textDocument = textDocument + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + def to_json(self): + return {"textDocument" : self.textDocument.to_json()} + +class HoverParams(TextDocumentPositionParams): + pass + +class SignatureHelpParams(TextDocumentPositionParams): + pass + +# Helper functions for writing tests + +def make_Range_for_token(line: int, + start_character: int, + end_character: int): + return Range(start=Position(line=line, character=start_character), + end=Position(line=line, character=end_character)) + +def make_Location_for_token(uri: str, + line: int, + start_character:int, + end_character:int): + return Location(uri=uri, + range_=make_Range_for_token(line, start_character, end_character)) + +""" +class Transport: + def post_request(self, data: str): + raise NotImplementedError + def post_notification(self, data: str): + raise NotImplementedError + +class UrlTransport(Transport): + def __init__(self, url): + self.url = url + + def post_request(self, data: str): + headers = {'content-type': 'application/json'} + response = requests.post(self.url, data=data, headers=headers) + return response + +class SubprocessTransport(Transport): + def __init__(self, args): + self.args = args + self.proc = subprocess.Popen(args, + bufsize=0, # unbuffered + stdin=subprocess.PIPE, + stdout=subprocess.PIPE) + + def post_request(self, data: str): + encoded = data.encode('utf-8') + byte_msg = f'Content-Length: {len(encoded)}\r\n\r\n'.encode('utf-8') + encoded + log('toy-ide', 'sending to subprocess: %r' % byte_msg) + self.proc.stdin.write(byte_msg) + self.proc.stdin.flush() + log('toy-ide', 'sent to subprocess') + #self.proc.stdout.flush() + resp = os.read(self.proc.stdout.fileno(), 4096) # FIXME + log('toy-ide', f'received from subprocess: {resp=}') + #headers = {'content-type': 'application/json'} + #response = requests.post(self.url, data=data, headers=headers) + #return response + + def post_notification(self, data: str): + encoded = data.encode('utf-8') + byte_msg = f'Content-Length: {len(encoded)}\r\n\r\n'.encode('utf-8') + encoded + log('toy-ide', 'sending to subprocess: %r' % byte_msg) + self.proc.stdin.write(byte_msg) + self.proc.stdin.flush() + log('toy-ide', 'sent to subprocess') + #resp = self.proc.stdout.read(); + #log('toy-ide', f'received from subprocess: {resp=}') + ''' + headers = {'content-type': 'application/json'} + response = requests.post(self.url, data=data, headers=headers) + return response + ''' + +# A wrapper for making LSP calls against a server + +class Proxy: + def __init__(self, transport: Transport): + self.transport = transport + self.next_id = 0 + + # TODO: initialization + params = InitializeParams() + json_resp = self.post_request('initialize', params.to_json()) + print(json_resp) + + def make_request(self, method, params): + json_req = {"method": method, + "params": params, + "jsonrpc": "2.0", + "id": self.next_id} + self.next_id += 1 + return json_req + + def make_notification(self, method, params): + json_req = {"method": method, + "params": params, + "jsonrpc": "2.0"} + return json_req + + def post_request(self, method, params): + json_obj = self.make_request(method, params) + response = self.transport.post_request(json.dumps(json_obj)) + #print('response: %r' % response) + #print('response.json(): %r' % response.json()) + #return response.json() + + def post_notification(self, method, params): + json_obj = self.make_notification(method, params) + response = self.transport.post_notification(json.dumps(json_obj)) + #print('response: %r' % response) + #print('response.json(): %r' % response.json()) + #return response.json() + + def textDocument_didOpen(self, params: DidOpenTextDocumentParams): + self.post_notification('textDocument/didOpen', + params.to_json()) + + def textDocument_definition(self, params: TextDocumentPositionParams): + json_resp = self.post_request('textDocument/definition', + params.to_json()) + print(f'{json_resp=}') + raise foo + # Expect either a Location or a list of Location + if isinstance(json_resp, list): + return [Location.from_json(item) for item in json_resp] + else: + return Location.from_json(json_resp) +""" + +class JsonRpcMessage: + def __init__(self, method: str, params, id=None): + self.method = method + self.params = params + self.id_ = id_ + + def __repr__(self): + return f'JsonRpcMessage({self.method:s}, {self.params}, {self.id_})' + + def to_json(self): + res = {"jsonrpc": "2.0", + "method": self.method, + "params": self.params} + if self.id_ is not None: + res['id'] = self.id_ + return res + + ''' + @staticmethod + def from_json_request(js_obj): + return JsonRpcMessage(method=js_obj['method'], + params=js_obj['params'], + id=js_obj.get('id')) + ''' + +class JsonRpcMessage: + def to_json(self): + raise NotImplementedError(); + +class JsonRpcRequest(JsonRpcMessage): + def __init__(self, method: str, params, id_=None): + self.method = method + self.params = params + self.id_ = id_ + + def __repr__(self): + return f'JsonRpcRequest({self.method:s}, {self.params}, {self.id_})' + + def to_json(self): + res = {"jsonrpc": "2.0", + "method": self.method, + "params": self.params} + if self.id_ is not None: + res['id'] = self.id_ + return res + +class ByteConnection: + def __init__(self, fd_receive, fd_send): + self.fd_receive = fd_receive + self.fd_send = fd_send + self.outgoing_data = bytes() + self.incoming_byte_handler = None + + def __repr__(self): + return f'ByteConnection({self.fd_receive}, {self.fd_send}, {self.outgoing_data}, {self.incoming_data})' + + @staticmethod + def from_subprocess(proc): + return ByteConnection(fd_receive = proc.stdout.fileno(), + fd_send = proc.stdin.fileno()) + + def push_data(self, data: bytes): + log('ByteConnection.push_data', repr(data)) + self.outgoing_data += data + + def pop_data(self, data: bytes): + result = self.outgoing_buffers[0] + self.outgoing_buffers = self.outgoing_buffers[1:] + return result + + def blocking_read(self): + received = os.read(self.fd_receive, 4096) + log('ByteConnection.on_receive', f'{len(received)} bytes read from fd {self.fd_receive}') + log('ByteConnection.on_receive', repr(received)) + if self.incoming_byte_handler: + self.incoming_byte_handler(received) + + def blocking_write(self): + if self.outgoing_data: + bytes_written = os.write(self.fd_send, self.outgoing_data) + self.outgoing_data = self.outgoing_data[bytes_written:] + log('ByteConnection.send', f'{bytes_written} bytes written to fd {self.fd_send}') + +class HttpMessage: + def __init__(self, content: bytes): + self.content = content + + def __repr__(self): + return f'HttpMessage({self.content})' + + def to_bytes(self) -> bytes: + return f'Content-Length: {len(self.content)}\r\n\r\n' + self.content + +class HttpParser: + def __init__(self): + self.phase = 'headers' + self.pending_data = bytes() + self.content_length = None + + def consume_octet(self, octet, on_http_message): + #log('HttpParser.consume_octet', repr(octet)) + self.pending_data += octet.to_bytes(length=1, byteorder='big') + #print(f'{self.pending_data=}') + if self.phase == 'headers': + if self.pending_data.endswith(b'\r\n'): + if self.pending_data == b'\r\n': + # blank line; end of headers + #log('HttpParser.consume_octet', 'blank line: end of headers') + self.pending_data = bytes() + self.phase = 'body' + else: + # Header (or initial verb line) + self.parse_header(self.pending_data[:-2]) + self.pending_data = bytes() + else: + assert self.phase == 'body' + assert self.content_length is not None + if len(self.pending_data) == self.content_length: + msg = HttpMessage(content=self.pending_data) + self.phase = 'headers' + self.pending_data = bytes() + self.content_length = None + if on_http_message: + on_http_message(msg) + + def parse_header(self, line: bytes): + # header-field = field-name ":" OWS field-value OWS + log('HttpParser.parse_header', repr(line)) + m = re.match(br'Content-Length:\s+([0-9]+)', line) + if m: + self.content_length = int(m.group(1)) + # FIXME: discard other headers for now + +class HttpConnection: + def __init__(self, byte_conn: ByteConnection): + self.byte_conn = byte_conn + self.outgoing_messages = [] + self.parser = HttpParser() + self.incoming_http_message_handler = None + + def push_message (self, msg: HttpMessage): + #log('HttpConnection.push_message', repr(msg)) + self.byte_conn.push_data(msg.to_bytes().encode('utf-8')) + + def on_incoming_bytes(self, data: bytes): + #log('HttpConnection.on_incoming_bytes', repr(data)) + for b in data: + self.parser.consume_octet(b, self.incoming_http_message_handler) + +class Selector: + def __init__(self): + self.byte_connections = set() + self.poll = select.poll() + self.fd_to_connection = {} + + def add_byte_connection(self, conn: ByteConnection): + self.byte_connections.add(conn) + self.poll.register(conn.fd_receive) + self.poll.register(conn.fd_send) + self.fd_to_connection[conn.fd_receive] = conn + self.fd_to_connection[conn.fd_send] = conn + + def serve_forever(self): + log('Selector.serve_forever', '') + while 1: + serve_pending_events () + + def serve_pending_events (self): + res = self.poll.poll() + for fd, event in res: + # print(f'{fd=} {event=}') + if event & select.POLLIN: + if 0: + print('POLLIN') + conn = self.fd_to_connection[fd] + #print(f'{conn=}') + conn.blocking_read() + if event & select.POLLOUT: + if 0: + print('POLLOUT') + conn = self.fd_to_connection[fd] + #print(f'{conn=}') + conn.blocking_write() + +class JsonRpcEndpoint: + def __init__(self, http: HttpConnection): + self.http = http + self.next_id = 0 + + # map from id to response objects + self.responses_by_id = {} + + self.incoming_notifications = [] + + # set of all ids for requests we haven't yet had a response from + self.pending_request_ids = set() + + def send_request(self, method_name: str, params): + log('JsonRpcEndpoint.send_request', method_name) + id_ = self.next_id + lsp_msg = JsonRpcRequest(method_name, params.to_json(), id_) + self.next_id += 1 + self.queue_message(lsp_msg) + return id_ + + def send_notification(self, method_name: str, params): + log('JsonRpcEndpoint.send_notification', method_name) + lsp_msg = JsonRpcRequest(method_name, params.to_json()) + self.queue_message(lsp_msg) + + def queue_message(self, msg: JsonRpcMessage): + log('JsonRpcEndpoint.queue_message', msg) + js_msg = json.dumps(msg.to_json()) + self.http.push_message(HttpMessage(content=js_msg)) + self.pending_request_ids.add(msg.id_) + + def on_incoming_http_message(self, http_msg: HttpMessage): + #log('JsonRpcEndpoint.on_incoming_http_message', repr(msg)) + js_msg = json.loads(http_msg.content.decode('utf-8')) + if 0: + print(f'{js_msg=}') + id_ = js_msg.get('id') + if id_ is None: + # notification + self.incoming_notifications.append(js_msg) + else: + # response + if id_ not in self.pending_request_ids: + raise ValueError('unexpected id: %s', id_) + self.responses_by_id[id_] = js_msg + self.pending_request_ids.remove(id_) + + def serve_pending_events(self): + s = Selector() + s.add_byte_connection(self.byte_conn) + s.serve_pending_events() + + def serve_until_response(self, id_): + """ + Block, sending and receiving data until we receive a response to + request with the given ID. + Return the response. + """ + # FIXME: will probably want a timeout here, or a count + #for i in range(10000): + while True: + self.serve_pending_events() + if id_ in self.responses_by_id: + return self.responses_by_id[id_] + raise TimeoutError('no response received for id:%i' % id_) + + def serve_until_notification(self, method: str): + """ + Block, sending and receiving data until we receive a notification + of the given method. + Return the notification. + """ + while True: + self.serve_pending_events() + for notif in self.incoming_notifications: + if notif['method'] == method: + return notif + +class GccLspClient(JsonRpcEndpoint): + def __init__(self, init=True): + if 0: + args=['clangd', '--log=verbose'] + else: + args = ['/home/david/nomad-coding/gcc-newgit-no-function-points/build/gcc/cc1', # FIXME + '-quiet', + '/home/david/nomad-coding/gcc-newgit-no-function-points/src/gcc/testsuite/gcc.dg/lsp/hover-1.c', # FIXME + '-fdiagnostics-color=never', # FIXME + '-flsp-server'] + self.server_subprocess = subprocess.Popen(args, + bufsize=0, # unbuffered + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + env={'LANG': 'C'}) + self.byte_conn = ByteConnection.from_subprocess(self.server_subprocess) + http_conn = HttpConnection(self.byte_conn) + self.byte_conn.incoming_byte_handler = http_conn.on_incoming_bytes + JsonRpcEndpoint.__init__(self, http_conn) + http_conn.incoming_http_message_handler = self.on_incoming_http_message + + # Start initialization handshake + if init: + initialize_id = self.send_request('initialize', + InitializeParams(processId=os.getpid(), + capabilities=ClientCapabilities())) + self.init_response = self.serve_until_response(id_=initialize_id)['result'] + if 0: + print(self.init_response) + if 'error' in self.init_response: + raise ValueError('server initialization failed: %s' + % self.init_response) + self.send_notification('initialized', InitializedParams()) + + def __enter__(self): + return self + + def __exit__(self, *args): + self.server_subprocess.terminate() + + def open_file(self, filename): + with open(filename) as f: + text = f.read() + uri = Path(filename).resolve().as_uri() + self.send_notification('textDocument/didOpen', + DidOpenTextDocumentParams(TextDocumentItem(uri=uri, + languageId='c', # FIXME + version=0, + text=text))) + return uri diff --git a/gcc/topics/ast-events-recorder.h b/gcc/topics/ast-events-recorder.h index 9100ccfadf5..3273ac9525f 100644 --- a/gcc/topics/ast-events-recorder.h +++ b/gcc/topics/ast-events-recorder.h @@ -35,11 +35,25 @@ public: { saved_node (const char *kind, location_t start_loc) : m_kind (kind), - m_start_loc (start_loc) + m_start_loc (start_loc), + m_end_loc (UNKNOWN_LOCATION), + m_expr (nullptr), + m_parent (nullptr) {} + template + void accept (NodeVisitor &visitor) + { + visitor.visit (*this); + for (auto &iter : m_children) + iter->accept (visitor); + } + const char *m_kind; location_t m_start_loc; + location_t m_end_loc; + tree m_expr; // FIXME: GC integration? + saved_node *m_parent; std::vector> m_children; }; @@ -55,14 +69,22 @@ public: gcc_assert (prev_top_of_stack); auto new_node = std::make_unique (m.kind, m.loc); m_stack.push_back (new_node.get ()); + new_node->m_parent = prev_top_of_stack; prev_top_of_stack->m_children.push_back (std::move (new_node)); } - void on_message (const ast_events::end_node &) final override + void on_message (const ast_events::set_tree &m) final override + { + gcc_assert (!m_stack.empty ()); + auto top_of_stack = m_stack.back (); + top_of_stack->m_expr = m.expr; + } + + void on_message (const ast_events::end_node &m) final override { gcc_assert (!m_stack.empty ()); + m_stack.back ()->m_end_loc = m.loc; m_stack.pop_back (); - // FIXME: also capture the endloc in each saved_node? } private: diff --git a/gcc/topics/ast-events.h b/gcc/topics/ast-events.h index ba7ebe42ff2..0d3bf829715 100644 --- a/gcc/topics/ast-events.h +++ b/gcc/topics/ast-events.h @@ -35,6 +35,11 @@ struct begin_node location_t loc; }; +struct set_tree +{ + tree expr; +}; + struct end_node { const char *kind; @@ -45,6 +50,7 @@ struct subscriber { virtual ~subscriber () {} virtual void on_message (const begin_node &) {} + virtual void on_message (const set_tree &) {} virtual void on_message (const end_node &) {} }; -- 2.49.0