From d6d65b5648d0e6b5b95967feeebe8da725c9da5a Mon Sep 17 00:00:00 2001 From: lexouduck Date: Fri, 1 Apr 2022 18:51:07 +0200 Subject: [PATCH 01/13] fix(plugin/error_vis/popups.py): fixed issue where hover popup would not display variadic argument ellipsis '...' --- plugin/error_vis/popups.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/plugin/error_vis/popups.py b/plugin/error_vis/popups.py index 78fad254..f384668c 100644 --- a/plugin/error_vis/popups.py +++ b/plugin/error_vis/popups.py @@ -105,6 +105,13 @@ def info(cursor, cindex, settings): ] is_macro = cursor.kind == cindex.CursorKind.MACRO_DEFINITION is_class_template = cursor.kind == cindex.CursorKind.CLASS_TEMPLATE + is_function = cursor.kind in [ + cindex.CursorKind.FUNCTION_DECL, + cindex.CursorKind.CXX_METHOD, + cindex.CursorKind.CONSTRUCTOR, + cindex.CursorKind.DESTRUCTOR, + cindex.CursorKind.CONVERSION_FUNCTION, + cindex.CursorKind.FUNCTION_TEMPLATE] # Show the return type of the function/method if applicable, # macros just show that they are a macro. @@ -156,12 +163,9 @@ def info(cursor, cindex, settings): args.append(arg_type_decl + " " + arg.spelling) else: args.append(arg_type_decl) - if cursor.kind in [cindex.CursorKind.FUNCTION_DECL, - cindex.CursorKind.CXX_METHOD, - cindex.CursorKind.CONSTRUCTOR, - cindex.CursorKind.DESTRUCTOR, - cindex.CursorKind.CONVERSION_FUNCTION, - cindex.CursorKind.FUNCTION_TEMPLATE]: + if is_function: + if cursor.type is not None and cursor.type.is_function_variadic(): + args.append("...") args_string = '(' if len(args): args_string += ', '.join(args) From da2592c122fb0d3fe39b5643c6b4ad2c04dc9b2b Mon Sep 17 00:00:00 2001 From: lexouduck Date: Fri, 1 Apr 2022 19:23:37 +0200 Subject: [PATCH 02/13] improvement(plugin/error_vis/popups.py): hover doc popup now displays function declaration with syntax color in 'Body' section --- plugin/error_vis/popups.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/plugin/error_vis/popups.py b/plugin/error_vis/popups.py index f384668c..03bdd16d 100644 --- a/plugin/error_vis/popups.py +++ b/plugin/error_vis/popups.py @@ -45,7 +45,7 @@ {content} """ -FULL_DOC_TEMPLATE = """### Full doxygen comment: +FULL_DOC_TEMPLATE = """### Detailed documentation: {content} """ @@ -187,11 +187,12 @@ def info(cursor, cindex, settings): popup.__text += Popup.__lookup_in_sublime_index( sublime.active_window(), cursor.spelling) - # Doxygen comments + # Doxygen comment: single-line brief description if cursor.brief_comment: popup.__text += BRIEF_DOC_TEMPLATE.format( content=CODE_TEMPLATE.format(lang="", code=cursor.brief_comment)) + # Doxygen comment: multi-line detailed description if cursor.raw_comment: clean_comment = Popup.cleanup_comment(cursor.raw_comment).strip() print(clean_comment) @@ -201,11 +202,34 @@ def info(cursor, cindex, settings): content=CODE_TEMPLATE.format(lang="", code=clean_comment)) # Show macro body if is_macro: + body = "#define " + body += cursor.spelling + body += macro_parser.args_string + body += macro_parser.body_string popup.__text += BODY_TEMPLATE.format( - content=CODE_TEMPLATE.format(lang="c++", - code=macro_parser.body_string)) + content=CODE_TEMPLATE.format(lang="c++", code=body)) + # Show function declaration + elif is_function: + body = cursor.result_type.spelling + body += " " + body += cursor.spelling + args = [] + for arg in cursor.get_arguments(): + if arg.spelling: + args.append(arg.type.spelling + " " + arg.spelling) + else: + args.append(arg.type.spelling) + if cursor.type is not None and cursor.type.is_function_variadic(): + args.append("...") + body += '(' + if len(args): + body += ', '.join(args) + body += ');' + body = Popup.prettify_body(body) + popup.__text += BODY_TEMPLATE.format( + content=CODE_TEMPLATE.format(lang="c++", code=body)) # Show type declaration - if settings.show_type_body and body_cursor and body_cursor.extent: + elif settings.show_type_body and body_cursor and body_cursor.extent: body = Popup.get_text_by_extent(body_cursor.extent) body = Popup.prettify_body(body) popup.__text += BODY_TEMPLATE.format( From 830452869bca55dcc0bdab54c00c96c1eb4cd80a Mon Sep 17 00:00:00 2001 From: lexouduck Date: Sat, 2 Apr 2022 21:06:13 +0200 Subject: [PATCH 03/13] fix(popups): fixed issue where popups would not appear when hovering over a macro definition #define statement --- plugin/completion/lib_complete.py | 3 +++ plugin/error_vis/popups.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/plugin/completion/lib_complete.py b/plugin/completion/lib_complete.py index 2dd93dd1..9dda976d 100644 --- a/plugin/completion/lib_complete.py +++ b/plugin/completion/lib_complete.py @@ -287,6 +287,9 @@ def info(self, tooltip_request, settings): info_popup = Popup.info( cursor.referenced, self.cindex, settings) return tooltip_request, info_popup + if cursor.kind == self.cindex.CursorKind.MACRO_DEFINITION: + info_popup = Popup.info(cursor, self.cindex, settings) + return tooltip_request, info_popup return empty_info def update(self, view, settings): diff --git a/plugin/error_vis/popups.py b/plugin/error_vis/popups.py index 03bdd16d..20ca883b 100644 --- a/plugin/error_vis/popups.py +++ b/plugin/error_vis/popups.py @@ -204,7 +204,7 @@ def info(cursor, cindex, settings): if is_macro: body = "#define " body += cursor.spelling - body += macro_parser.args_string + body += macro_parser.args_string if len(macro_parser.args_string) else " " body += macro_parser.body_string popup.__text += BODY_TEMPLATE.format( content=CODE_TEMPLATE.format(lang="c++", code=body)) From 1e430d4563028699d825d3df338bbaca913cc28c Mon Sep 17 00:00:00 2001 From: lexouduck Date: Sun, 3 Apr 2022 16:11:54 +0200 Subject: [PATCH 04/13] fix(macro_parser.py): added parsing logic to show documentation comments for macros, despite clang not doing this for us --- plugin/error_vis/popups.py | 9 +++-- plugin/utils/macro_parser.py | 66 ++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/plugin/error_vis/popups.py b/plugin/error_vis/popups.py index 20ca883b..38974cfb 100644 --- a/plugin/error_vis/popups.py +++ b/plugin/error_vis/popups.py @@ -187,13 +187,18 @@ def info(cursor, cindex, settings): popup.__text += Popup.__lookup_in_sublime_index( sublime.active_window(), cursor.spelling) + raw_comment = None + if is_macro: + raw_comment = macro_parser.doc_string + else: + raw_comment = cursor.raw_comment # Doxygen comment: single-line brief description - if cursor.brief_comment: + if raw_comment and cursor.brief_comment: popup.__text += BRIEF_DOC_TEMPLATE.format( content=CODE_TEMPLATE.format(lang="", code=cursor.brief_comment)) # Doxygen comment: multi-line detailed description - if cursor.raw_comment: + if raw_comment and cursor.raw_comment: clean_comment = Popup.cleanup_comment(cursor.raw_comment).strip() print(clean_comment) if clean_comment: diff --git a/plugin/utils/macro_parser.py b/plugin/utils/macro_parser.py index a3ddc1f5..046ed27d 100644 --- a/plugin/utils/macro_parser.py +++ b/plugin/utils/macro_parser.py @@ -1,5 +1,9 @@ """Parse a macro from cindex.""" +import re +import logging + +log = logging.getLogger("ECC") class MacroParser(object): """Parse info from macros. @@ -21,6 +25,7 @@ def __init__(self, name, location): in a macro with parenthesis, continue parsing into the next line to find it and create a proper args string. """ + self._raw_comment = '' self._args_string = '' self._name = name self._body = '' @@ -38,6 +43,52 @@ def _parse_macro_file_lines(self, macro_file_lines, macro_line_number): macro_line_number (int): line number (1-based) of the macro in macro_file_lines. """ + # parse doxygen comment above macro definition + self._raw_comment = "" + parser_state = 0 + # (parser_state == 0) -> no comment encountered yet + # (parser_state == 1) -> single-line comment encountered + # (parser_state == 2) -> multi-line comment encountered + # (parser_state == 3) -> comment found, finished + lineno = macro_line_number - 1 + while (lineno > 0): + # parse the preceding line of text + lineno -= 1 + if (lineno == 0): + break + prevline = macro_file_lines[lineno].lstrip() + # skip any `#if`/`#ifdef` guards before the macro `#define` line, if applicable + if re.match(r'^[ \t]*#[ \t]*(if|elif|else|ifn?def)[ \t]+', prevline): + continue + # parse single-line comments + if (parser_state != 2) and re.match(r'^\s*//', prevline): + parser_state = 1 + if (re.match(r'^\s*//!', prevline) or re.match(r'^\s*///', prevline)): + self._raw_comment = prevline + "\n" + self._raw_comment + else: + parser_state = 3 + log.debug("Error while parsing macro doc comment: found normal single-line comment: " + self._raw_comment) + parser_state = 0 if len(self._raw_comment) == 0 else 3 + continue + # parse multi-line comments + if (parser_state == 2): + if re.match(r'^\s*/\*', prevline): + if re.match(r'^\s*/\*[\*!]', prevline): + self._raw_comment = prevline + "\n" + self._raw_comment + else: + log.debug("Error while parsing macro doc comment: found normal multi-line comment: " + self._raw_comment) + parser_state = 0 if len(self._raw_comment) == 0 else 3 + else: + self._raw_comment = prevline + "\n" + self._raw_comment + continue + elif re.match(r'^\s*\*/', prevline): + self._raw_comment = prevline + "\n" + self._raw_comment + parser_state = 2 + continue + if (len(prevline) > 0): + log.debug("Error while parsing macro doc comment: found " + prevline) + break + macro_line = macro_file_lines[macro_line_number - 1].strip() # strip leading '#define' macro_line = macro_line.lstrip('#').lstrip().lstrip('define') @@ -79,3 +130,18 @@ def args_string(self): def body_string(self): """Get macro body string.""" return self._body + + @property + def doc_string(self): + """Get documentation comment string. + + This follows conventional doxygen syntax, so your comment can use any of these syntaxes: + - /** doc comment (Java style) */ + - /// doc comment (C# style) + - //! doc comment (Qt style), single-line + - /*! doc comment (Qt style), block */ + This means that the following comment syntaxes are NOT valid documentation comments: + - // normal single-line comment + - /* normal block comment */ + """ + return self._raw_comment From 3351c3708b31b0cbcded869a8e77c8c7659755cf Mon Sep 17 00:00:00 2001 From: lexouduck Date: Sun, 3 Apr 2022 17:48:55 +0200 Subject: [PATCH 05/13] fix: have the new function 'body' section respect the 'show_body' user setting --- plugin/error_vis/popups.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/error_vis/popups.py b/plugin/error_vis/popups.py index 38974cfb..a3a03bde 100644 --- a/plugin/error_vis/popups.py +++ b/plugin/error_vis/popups.py @@ -214,7 +214,7 @@ def info(cursor, cindex, settings): popup.__text += BODY_TEMPLATE.format( content=CODE_TEMPLATE.format(lang="c++", code=body)) # Show function declaration - elif is_function: + elif settings.show_type_body and is_function: body = cursor.result_type.spelling body += " " body += cursor.spelling From 8b2e10ead6d707e3a9b9c51acecfb34e1bc492fa Mon Sep 17 00:00:00 2001 From: lexouduck Date: Sun, 3 Apr 2022 17:50:31 +0200 Subject: [PATCH 06/13] fix,ci: updated test for mdpopup new section name: 'Full doxygen comment' -> 'Detailed documentation' --- tests/test_error_vis.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_error_vis.py b/tests/test_error_vis.py index 40234419..322b3d1b 100644 --- a/tests/test_error_vis.py +++ b/tests/test_error_vis.py @@ -330,7 +330,7 @@ def test_info_full(self): ``` This is short. ``` - ### Full doxygen comment: + ### Detailed documentation: ``` And this is a full comment. From 58d9ffbe58ba988e55bd79af72bfc58970935920 Mon Sep 17 00:00:00 2001 From: lexouduck Date: Sun, 3 Apr 2022 18:04:03 +0200 Subject: [PATCH 07/13] fix,ci,style: formatting fixes - changed code lines to all be fewer than 80 chars --- plugin/error_vis/popups.py | 8 ++++++-- plugin/utils/macro_parser.py | 26 ++++++++++++++++++-------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/plugin/error_vis/popups.py b/plugin/error_vis/popups.py index a3a03bde..24f59f1c 100644 --- a/plugin/error_vis/popups.py +++ b/plugin/error_vis/popups.py @@ -164,7 +164,8 @@ def info(cursor, cindex, settings): else: args.append(arg_type_decl) if is_function: - if cursor.type is not None and cursor.type.is_function_variadic(): + if (cursor.type is not None and + cursor.type.is_function_variadic()): args.append("...") args_string = '(' if len(args): @@ -209,7 +210,10 @@ def info(cursor, cindex, settings): if is_macro: body = "#define " body += cursor.spelling - body += macro_parser.args_string if len(macro_parser.args_string) else " " + if (len(macro_parser.args_string) > 0): + body += macro_parser.args_string + else: + body += " " body += macro_parser.body_string popup.__text += BODY_TEMPLATE.format( content=CODE_TEMPLATE.format(lang="c++", code=body)) diff --git a/plugin/utils/macro_parser.py b/plugin/utils/macro_parser.py index 046ed27d..6bcdcc31 100644 --- a/plugin/utils/macro_parser.py +++ b/plugin/utils/macro_parser.py @@ -5,6 +5,7 @@ log = logging.getLogger("ECC") + class MacroParser(object): """Parse info from macros. @@ -57,17 +58,21 @@ def _parse_macro_file_lines(self, macro_file_lines, macro_line_number): if (lineno == 0): break prevline = macro_file_lines[lineno].lstrip() - # skip any `#if`/`#ifdef` guards before the macro `#define` line, if applicable - if re.match(r'^[ \t]*#[ \t]*(if|elif|else|ifn?def)[ \t]+', prevline): + # skip any #if or #ifdef guards before the #define, if applicable + if re.match(r'^[ \t]*#[ \t]*(if|elif|else|ifn?def)[ \t]+', + prevline): continue # parse single-line comments if (parser_state != 2) and re.match(r'^\s*//', prevline): parser_state = 1 - if (re.match(r'^\s*//!', prevline) or re.match(r'^\s*///', prevline)): + if (re.match(r'^\s*//!', prevline) or + re.match(r'^\s*///', prevline)): self._raw_comment = prevline + "\n" + self._raw_comment else: parser_state = 3 - log.debug("Error while parsing macro doc comment: found normal single-line comment: " + self._raw_comment) + log.debug("Error while parsing macro doc comment, " + + "found normal single-line comment: " + + self._raw_comment) parser_state = 0 if len(self._raw_comment) == 0 else 3 continue # parse multi-line comments @@ -76,7 +81,9 @@ def _parse_macro_file_lines(self, macro_file_lines, macro_line_number): if re.match(r'^\s*/\*[\*!]', prevline): self._raw_comment = prevline + "\n" + self._raw_comment else: - log.debug("Error while parsing macro doc comment: found normal multi-line comment: " + self._raw_comment) + log.debug("Error while parsing macro doc comment, " + + "found normal multi-line comment: " + + self._raw_comment) parser_state = 0 if len(self._raw_comment) == 0 else 3 else: self._raw_comment = prevline + "\n" + self._raw_comment @@ -86,7 +93,8 @@ def _parse_macro_file_lines(self, macro_file_lines, macro_line_number): parser_state = 2 continue if (len(prevline) > 0): - log.debug("Error while parsing macro doc comment: found " + prevline) + log.debug("Error while parsing macro doc comment, " + + "found: " + prevline) break macro_line = macro_file_lines[macro_line_number - 1].strip() @@ -135,12 +143,14 @@ def body_string(self): def doc_string(self): """Get documentation comment string. - This follows conventional doxygen syntax, so your comment can use any of these syntaxes: + This follows conventional doxygen syntax, + so your comment can use any of these syntaxes: - /** doc comment (Java style) */ - /// doc comment (C# style) - //! doc comment (Qt style), single-line - /*! doc comment (Qt style), block */ - This means that the following comment syntaxes are NOT valid documentation comments: + Conversely, this means that the following comment + syntaxes are NOT valid documentation comments: - // normal single-line comment - /* normal block comment */ """ From 83f6b68ef647399cc523f33ea5d29c4b27348a1a Mon Sep 17 00:00:00 2001 From: lexouduck Date: Sun, 3 Apr 2022 18:24:14 +0200 Subject: [PATCH 08/13] fix,ci,test: updated tests to work with the new 'Body' section for functions in mdpopups --- tests/test_error_vis.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_error_vis.py b/tests/test_error_vis.py index 322b3d1b..9a16e6e3 100644 --- a/tests/test_error_vis.py +++ b/tests/test_error_vis.py @@ -245,6 +245,10 @@ def test_info_simple(self): !!! panel-info "ECC: Info" ## Declaration: int [main]({file}:7:5) (int argc, const char *[] argv) + ### Body: + ```c++ + int main(int argc, const char *[] argv); + ``` """.format(file=file_name) self.assertEqual(info_popup.as_markdown(), expected_info_msg) # cleanup @@ -337,6 +341,10 @@ def test_info_full(self): @param[in] a param a @param[in] b param b ``` + ### Body: + ```c++ + void foo(int a, int b); + ``` """.format(file=file_name) # Make sure we remove trailing spaces on the right to comply with how # sublime text handles this. @@ -372,6 +380,10 @@ def test_info_arguments_link(self): !!! panel-info "ECC: Info" ## Declaration: void [foo]({file}:5:8) ([Foo]({file}:1:7) a, [Foo]({file}:1:7) \\* b) + ### Body: + ```c++ + void foo(Foo a, Foo * b); + ``` """.format(file=file_name) # Make sure we remove trailing spaces on the right to comply with how # sublime text handles this. @@ -1191,6 +1203,10 @@ def test_method_with_template_argument(self): ## Declaration: void [foo]({file}:6:8) ([TemplateClass]({file}:3:7)<Foo \ &&, int, 12>) + ### Body: + ```c++ + void foo(TemplateClass); + ``` """ expected_info_msg = fmt.format(file=file_name) # Make sure we remove trailing spaces on the right to comply with how From f9bf50cfcab4ee0e9419369a1f8355bbd4c2b9b7 Mon Sep 17 00:00:00 2001 From: lexouduck Date: Sun, 3 Apr 2022 18:54:44 +0200 Subject: [PATCH 09/13] style,ci: made code conform to PEP style checker --- plugin/error_vis/popups.py | 6 ++++-- plugin/utils/macro_parser.py | 16 +++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/plugin/error_vis/popups.py b/plugin/error_vis/popups.py index 24f59f1c..0177a20c 100644 --- a/plugin/error_vis/popups.py +++ b/plugin/error_vis/popups.py @@ -164,8 +164,10 @@ def info(cursor, cindex, settings): else: args.append(arg_type_decl) if is_function: - if (cursor.type is not None and - cursor.type.is_function_variadic()): + is_variadic = False + if (cursor.type is not None): + is_variadic = cursor.type.is_function_variadic() + if is_variadic: args.append("...") args_string = '(' if len(args): diff --git a/plugin/utils/macro_parser.py b/plugin/utils/macro_parser.py index 6bcdcc31..23abccce 100644 --- a/plugin/utils/macro_parser.py +++ b/plugin/utils/macro_parser.py @@ -59,20 +59,18 @@ def _parse_macro_file_lines(self, macro_file_lines, macro_line_number): break prevline = macro_file_lines[lineno].lstrip() # skip any #if or #ifdef guards before the #define, if applicable - if re.match(r'^[ \t]*#[ \t]*(if|elif|else|ifn?def)[ \t]+', - prevline): + if re.match(r'^[ \t]*#[ \t]*(if|elif|else|ifn?def)\s', prevline): continue # parse single-line comments if (parser_state != 2) and re.match(r'^\s*//', prevline): parser_state = 1 - if (re.match(r'^\s*//!', prevline) or - re.match(r'^\s*///', prevline)): + if (re.match(r'^\s*//[/!]', prevline)): self._raw_comment = prevline + "\n" + self._raw_comment else: parser_state = 3 log.debug("Error while parsing macro doc comment, " + - "found normal single-line comment: " + - self._raw_comment) + "found normal single-line comment: " + + self._raw_comment) parser_state = 0 if len(self._raw_comment) == 0 else 3 continue # parse multi-line comments @@ -82,8 +80,8 @@ def _parse_macro_file_lines(self, macro_file_lines, macro_line_number): self._raw_comment = prevline + "\n" + self._raw_comment else: log.debug("Error while parsing macro doc comment, " + - "found normal multi-line comment: " + - self._raw_comment) + "found normal multi-line comment: " + + self._raw_comment) parser_state = 0 if len(self._raw_comment) == 0 else 3 else: self._raw_comment = prevline + "\n" + self._raw_comment @@ -94,7 +92,7 @@ def _parse_macro_file_lines(self, macro_file_lines, macro_line_number): continue if (len(prevline) > 0): log.debug("Error while parsing macro doc comment, " + - "found: " + prevline) + "found: " + prevline) break macro_line = macro_file_lines[macro_line_number - 1].strip() From f31bd70f0739545f12d696b903cc320287016249 Mon Sep 17 00:00:00 2001 From: lexouduck Date: Sun, 3 Apr 2022 19:10:55 +0200 Subject: [PATCH 10/13] fix: minor fix for the mdpopup display for macro doc comments --- plugin/error_vis/popups.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugin/error_vis/popups.py b/plugin/error_vis/popups.py index 0177a20c..c060f0ca 100644 --- a/plugin/error_vis/popups.py +++ b/plugin/error_vis/popups.py @@ -190,19 +190,19 @@ def info(cursor, cindex, settings): popup.__text += Popup.__lookup_in_sublime_index( sublime.active_window(), cursor.spelling) - raw_comment = None + has_comment = None if is_macro: - raw_comment = macro_parser.doc_string + has_comment = macro_parser.doc_string else: - raw_comment = cursor.raw_comment + has_comment = cursor.raw_comment # Doxygen comment: single-line brief description - if raw_comment and cursor.brief_comment: + if has_comment and cursor.brief_comment: popup.__text += BRIEF_DOC_TEMPLATE.format( content=CODE_TEMPLATE.format(lang="", code=cursor.brief_comment)) # Doxygen comment: multi-line detailed description - if raw_comment and cursor.raw_comment: - clean_comment = Popup.cleanup_comment(cursor.raw_comment).strip() + if has_comment and cursor.raw_comment: + clean_comment = Popup.cleanup_comment(has_comment).strip() print(clean_comment) if clean_comment: # Only add this if there is a Doxygen comment. From 2204547153e2d2e1f0cab6ceaaf12a7e461cfdbc Mon Sep 17 00:00:00 2001 From: lexouduck Date: Sun, 3 Apr 2022 19:10:55 +0200 Subject: [PATCH 11/13] fix: bugfix for the mdpopup display for macro doc comments --- plugin/error_vis/popups.py | 13 +++++++------ plugin/utils/macro_parser.py | 10 +++++----- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/plugin/error_vis/popups.py b/plugin/error_vis/popups.py index 0177a20c..bcd2adef 100644 --- a/plugin/error_vis/popups.py +++ b/plugin/error_vis/popups.py @@ -190,24 +190,25 @@ def info(cursor, cindex, settings): popup.__text += Popup.__lookup_in_sublime_index( sublime.active_window(), cursor.spelling) - raw_comment = None + has_comment = None if is_macro: - raw_comment = macro_parser.doc_string + has_comment = macro_parser.doc_string else: - raw_comment = cursor.raw_comment + has_comment = cursor.raw_comment # Doxygen comment: single-line brief description - if raw_comment and cursor.brief_comment: + if cursor.brief_comment or (is_macro and has_comment): popup.__text += BRIEF_DOC_TEMPLATE.format( content=CODE_TEMPLATE.format(lang="", code=cursor.brief_comment)) # Doxygen comment: multi-line detailed description - if raw_comment and cursor.raw_comment: - clean_comment = Popup.cleanup_comment(cursor.raw_comment).strip() + if cursor.raw_comment or (is_macro and has_comment): + clean_comment = Popup.cleanup_comment(has_comment).strip() print(clean_comment) if clean_comment: # Only add this if there is a Doxygen comment. popup.__text += FULL_DOC_TEMPLATE.format( content=CODE_TEMPLATE.format(lang="", code=clean_comment)) + # Show macro body if is_macro: body = "#define " diff --git a/plugin/utils/macro_parser.py b/plugin/utils/macro_parser.py index 23abccce..7b34099d 100644 --- a/plugin/utils/macro_parser.py +++ b/plugin/utils/macro_parser.py @@ -65,7 +65,7 @@ def _parse_macro_file_lines(self, macro_file_lines, macro_line_number): if (parser_state != 2) and re.match(r'^\s*//', prevline): parser_state = 1 if (re.match(r'^\s*//[/!]', prevline)): - self._raw_comment = prevline + "\n" + self._raw_comment + self._raw_comment = prevline + self._raw_comment else: parser_state = 3 log.debug("Error while parsing macro doc comment, " + @@ -77,17 +77,17 @@ def _parse_macro_file_lines(self, macro_file_lines, macro_line_number): if (parser_state == 2): if re.match(r'^\s*/\*', prevline): if re.match(r'^\s*/\*[\*!]', prevline): - self._raw_comment = prevline + "\n" + self._raw_comment + self._raw_comment = prevline + self._raw_comment else: log.debug("Error while parsing macro doc comment, " + "found normal multi-line comment: " + self._raw_comment) parser_state = 0 if len(self._raw_comment) == 0 else 3 else: - self._raw_comment = prevline + "\n" + self._raw_comment + self._raw_comment = prevline + self._raw_comment continue elif re.match(r'^\s*\*/', prevline): - self._raw_comment = prevline + "\n" + self._raw_comment + self._raw_comment = prevline + self._raw_comment parser_state = 2 continue if (len(prevline) > 0): @@ -119,7 +119,7 @@ def _parse_macro_file_lines(self, macro_file_lines, macro_line_number): while self._body.endswith("\\"): macro_line_number += 1 line = macro_file_lines[macro_line_number - 1].rstrip() - self._body += "\n" + line + self._body += line @property def args_string(self): From bae5eee0e2d60abb889d9897057d267fd6e653c1 Mon Sep 17 00:00:00 2001 From: lexouduck Date: Mon, 27 Jun 2022 17:26:59 +0200 Subject: [PATCH 12/13] feature(popups.py): implemented two new settings which relate to MDpopup hoverdoc: one the reorder the sections in the popup, and another setting to display the documentation body text as markdown, rather than plaintext --- .gitignore | 3 + EasyClangComplete.sublime-settings | 15 ++ docs/settings.md | 27 +++ plugin/error_vis/popups.py | 271 +++++++++++++++++++--------- plugin/settings/settings_storage.py | 4 +- tests/test_error_vis.py | 3 + 6 files changed, 241 insertions(+), 82 deletions(-) diff --git a/.gitignore b/.gitignore index 3adadd74..6257c801 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ EasyClangComplete.sublime-workspace tests/bazel/good_project/bazel-* *compile_commands.json tests/bazel/bad_project/bazel-* + +package-metadata.json + diff --git a/EasyClangComplete.sublime-settings b/EasyClangComplete.sublime-settings index 1bd0dba1..2e7282de 100644 --- a/EasyClangComplete.sublime-settings +++ b/EasyClangComplete.sublime-settings @@ -87,6 +87,16 @@ "popup_maximum_width": 1800, "popup_maximum_height": 800, + // This array determines which sections should appear inside info popups, + // and in what order they should be displayed within the popup. + // By default, all of the possible sections are present. + "popup_sections": [ + "Declaration", + "References", + "Documentation", + "Body", + ], + // Triggers for auto-completion "triggers" : [ ".", "->", "::", " ", " ", "(", "[" ], @@ -170,6 +180,11 @@ // the symbol under cursor taking them from Sublime Text index. "show_index_references": true, + // Makes any documentation comments show up as rendered Markdown in the popup. + // By default, this value is set to `false`, which means that any documentation + // comments will be displayed within a literal text block (```). + "show_doc_as_markdown": false, + // When an includes trigger is typed (" or <) a quick panel will appear that // will guide the user in picking their includes based on the current // compilation database' include flags. diff --git a/docs/settings.md b/docs/settings.md index 8db64696..16239935 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -261,6 +261,22 @@ Setting that controls the maximum height of the popups generated by the plugin. "popup_maximum_height": 800, ``` +### **`popup_sections`** + +This array determines which sections should appear inside info popups, +and in what order they should be displayed within the popup. +By default, all of the possible sections are present. + +!!! example "Default value" + ```json + "popup_sections": [ + "Declaration", + "References", + "Documentation", + "Body", + ], + ``` + ### **`triggers`** Defines all characters that trigger auto-completion. The default value is: @@ -459,6 +475,17 @@ symbol under cursor taking them from Sublime Text index. "show_index_references": true, ``` +### **`show_doc_as_markdown`** + +Makes any documentation comments show up as rendered Markdown in the popup. +By default, this value is set to `false`, which means that any documentation +comments will be displayed within a literal text block (triple-backquotes). + +!!! example "Default value" + ```json + "show_doc_as_markdown": false, + ``` + ### **`autocomplete_includes`** diff --git a/plugin/error_vis/popups.py b/plugin/error_vis/popups.py index c060f0ca..518abe77 100644 --- a/plugin/error_vis/popups.py +++ b/plugin/error_vis/popups.py @@ -1,5 +1,6 @@ """Incapsulate popup creation.""" +import string import sublime import mdpopups import markupsafe @@ -94,38 +95,54 @@ def info(cursor, cindex, settings): settings.popup_maximum_width, settings.popup_maximum_height )) popup.__popup_type = 'panel-info "ECC: Info"' - is_type_decl = cursor.kind in [ - cindex.CursorKind.STRUCT_DECL, - cindex.CursorKind.UNION_DECL, - cindex.CursorKind.CLASS_DECL, - cindex.CursorKind.ENUM_DECL, - cindex.CursorKind.TYPEDEF_DECL, - cindex.CursorKind.TYPE_ALIAS_DECL, - cindex.CursorKind.TYPE_REF - ] - is_macro = cursor.kind == cindex.CursorKind.MACRO_DEFINITION - is_class_template = cursor.kind == cindex.CursorKind.CLASS_TEMPLATE + + macro_parser = None + if cursor.kind == cindex.CursorKind.MACRO_DEFINITION: + macro_parser = MacroParser(cursor.spelling, cursor.location) + + if not isinstance(settings.popup_sections, list): + log.error("Bad config value: \"popup_sections\" " + + "should be a list of strings") + elif len(settings.popup_sections) == 0: + log.error("Bad config value: \"popup_sections\" " + + "setting should have at least one element") + else: + popup.__text = "" + for i in settings.popup_sections: + if not isinstance(i, str): + log.error("Bad config value: \"popup_sections\" " + + "should be a list containing only strings") + elif re.match(r'[Dd]eclaration', i): + popup.__text += Popup.info_section_declaration( + cursor, cindex, settings, macro_parser) + elif re.match(r'[Rr]eferences', i): + popup.__text += Popup.info_section_references( + cursor, cindex, settings, macro_parser) + elif re.match(r'[Dd]ocumentation', i): + popup.__text += Popup.info_section_documentation( + cursor, cindex, settings, macro_parser) + elif re.match(r'([Bb]ody|[Ss]ource)', i): + popup.__text += Popup.info_section_body( + cursor, cindex, settings, macro_parser) + else: + log.error("Bad config value: \"popup_sections\" " + + "has unknown value: \"" + i + "\"") + + return popup + + @staticmethod + def info_section_declaration(cursor, cindex, settings, macro_parser): + """Generate the info text for the declaration.""" is_function = cursor.kind in [ cindex.CursorKind.FUNCTION_DECL, cindex.CursorKind.CXX_METHOD, cindex.CursorKind.CONSTRUCTOR, cindex.CursorKind.DESTRUCTOR, cindex.CursorKind.CONVERSION_FUNCTION, - cindex.CursorKind.FUNCTION_TEMPLATE] - - # Show the return type of the function/method if applicable, - # macros just show that they are a macro. - macro_parser = None - body_cursor = None - if is_type_decl: - body_cursor = cursor - elif is_class_template: - body_cursor = cursor.get_definition() - - # Initialize the text the declaration. + cindex.CursorKind.FUNCTION_TEMPLATE + ] declaration_text = '' - if is_macro: - macro_parser = MacroParser(cursor.spelling, cursor.location) + if macro_parser is not None: declaration_text += r'\#define ' else: if cursor.result_type.spelling: @@ -150,7 +167,7 @@ def info(cursor, cindex, settings): declaration_text += cursor.spelling # Macro/function/method arguments args_string = None - if is_macro: + if macro_parser is not None: # cursor.get_arguments() doesn't give us anything for macros, # so we have to parse those ourselves args_string = macro_parser.args_string @@ -183,45 +200,124 @@ def info(cursor, cindex, settings): if cursor.is_const_method(): declaration_text += " const" # Save declaration text. - popup.__text = DECLARATION_TEMPLATE.format( + return DECLARATION_TEMPLATE.format( type_declaration=markupsafe.escape(declaration_text)) - if settings.show_index_references: - popup.__text += Popup.__lookup_in_sublime_index( - sublime.active_window(), cursor.spelling) + @staticmethod + def info_section_references(cursor, cindex, settings, macro_parser): + """Generate the info text for the declaration.""" + window = sublime.active_window() + spelling = cursor.spelling + if not settings.show_index_references: + return "" + def lookup(lookup_function, spelling): + index = lookup_function(spelling) + references = [] + for location_tuple in index: + location = IndexLocation(filename=location_tuple[0], + line=location_tuple[2][0], + column=location_tuple[2][1]) + references.append( + "{reference}: `{file}:{line}:{col}`".format( + reference=Popup.link_from_location(location, spelling), + file=location.file.short_name, + line=location.line, + col=location.column)) + return markupsafe.escape("\n - ".join(references)) + + index_references = lookup(window.lookup_symbol_in_index, spelling) + usage_references = lookup(window.lookup_symbol_in_open_files, spelling) + output_text = "" + if index_references: + output_text += INDEX_REFERENCES_TEMPLATE.format( + references=" - " + index_references) + if usage_references: + output_text += OPEN_FILES_REFERENCES_TEMPLATE.format( + references=" - " + usage_references) + return output_text + + @staticmethod + def info_section_documentation(cursor, cindex, settings, macro_parser): + """Generate text for documentation comment(s), if any.""" + documentation_text = "" has_comment = None - if is_macro: + if macro_parser is not None: has_comment = macro_parser.doc_string else: has_comment = cursor.raw_comment - # Doxygen comment: single-line brief description - if has_comment and cursor.brief_comment: - popup.__text += BRIEF_DOC_TEMPLATE.format( - content=CODE_TEMPLATE.format(lang="", - code=cursor.brief_comment)) - # Doxygen comment: multi-line detailed description - if has_comment and cursor.raw_comment: - clean_comment = Popup.cleanup_comment(has_comment).strip() - print(clean_comment) - if clean_comment: - # Only add this if there is a Doxygen comment. - popup.__text += FULL_DOC_TEMPLATE.format( - content=CODE_TEMPLATE.format(lang="", code=clean_comment)) + if has_comment: + if settings.show_doc_as_markdown: + # Doxygen comment: single-line brief description + charset_comment = '/' + '*' + '!' + string.whitespace + brief_comment = has_comment.split("\n")[0] + brief_comment = brief_comment.lstrip(charset_comment) + if len(brief_comment) > 0: + brief_comment = Popup.doxygen_comment(brief_comment) + documentation_text += BRIEF_DOC_TEMPLATE.format( + content=brief_comment) + # Doxygen comment: multi-line detailed description + mdcomment = Popup.cleanup_comment(has_comment) + if len(mdcomment) > 0: + mdcomment = Popup.doxygen_comment(mdcomment) + # Only add this if there is a Doxygen comment. + documentation_text += FULL_DOC_TEMPLATE.format( + content=mdcomment) + else: + # Doxygen comment: single-line brief description + if cursor.brief_comment: + documentation_text += BRIEF_DOC_TEMPLATE.format( + content=CODE_TEMPLATE.format(code=cursor.brief_comment, + lang="")) + # Doxygen comment: multi-line detailed description + if cursor.raw_comment: + clean_comment = Popup.cleanup_comment(has_comment).strip() + if clean_comment: + # Only add this if there is a Doxygen comment. + documentation_text += FULL_DOC_TEMPLATE.format( + content=CODE_TEMPLATE.format(code=clean_comment, + lang="")) + log.debug("Processed comment:\n" + documentation_text) + return documentation_text + + @staticmethod + def info_section_body(cursor, cindex, settings, macro_parser): + """Generate info text for the "body" section.""" + is_type_decl = cursor.kind in [ + cindex.CursorKind.STRUCT_DECL, + cindex.CursorKind.UNION_DECL, + cindex.CursorKind.CLASS_DECL, + cindex.CursorKind.ENUM_DECL, + cindex.CursorKind.TYPEDEF_DECL, + cindex.CursorKind.TYPE_ALIAS_DECL, + cindex.CursorKind.TYPE_REF + ] + is_function = cursor.kind in [ + cindex.CursorKind.FUNCTION_DECL, + cindex.CursorKind.CXX_METHOD, + cindex.CursorKind.CONSTRUCTOR, + cindex.CursorKind.DESTRUCTOR, + cindex.CursorKind.CONVERSION_FUNCTION, + cindex.CursorKind.FUNCTION_TEMPLATE + ] + body_cursor = None + if is_type_decl: + body_cursor = cursor + elif cursor.kind == cindex.CursorKind.CLASS_TEMPLATE: + body_cursor = cursor.get_definition() + body = "" # Show macro body - if is_macro: - body = "#define " + if macro_parser is not None: + body += "#define " body += cursor.spelling if (len(macro_parser.args_string) > 0): body += macro_parser.args_string else: body += " " body += macro_parser.body_string - popup.__text += BODY_TEMPLATE.format( - content=CODE_TEMPLATE.format(lang="c++", code=body)) # Show function declaration elif settings.show_type_body and is_function: - body = cursor.result_type.spelling + body += cursor.result_type.spelling body += " " body += cursor.spelling args = [] @@ -237,42 +333,17 @@ def info(cursor, cindex, settings): body += ', '.join(args) body += ');' body = Popup.prettify_body(body) - popup.__text += BODY_TEMPLATE.format( - content=CODE_TEMPLATE.format(lang="c++", code=body)) # Show type declaration elif settings.show_type_body and body_cursor and body_cursor.extent: body = Popup.get_text_by_extent(body_cursor.extent) body = Popup.prettify_body(body) - popup.__text += BODY_TEMPLATE.format( - content=CODE_TEMPLATE.format(lang="c++", code=body)) - return popup - @staticmethod - def __lookup_in_sublime_index(window, spelling): - def lookup(lookup_function, spelling): - index = lookup_function(spelling) - references = [] - for location_tuple in index: - location = IndexLocation(filename=location_tuple[0], - line=location_tuple[2][0], - column=location_tuple[2][1]) - references.append( - "{reference}: `{file}:{line}:{col}`".format( - reference=Popup.link_from_location(location, spelling), - file=location.file.short_name, - line=location.line, - col=location.column)) - return markupsafe.escape("\n - ".join(references)) - index_references = lookup(window.lookup_symbol_in_index, spelling) - usage_references = lookup(window.lookup_symbol_in_open_files, spelling) - output_text = "" - if index_references: - output_text += INDEX_REFERENCES_TEMPLATE.format( - references=" - " + index_references) - if usage_references: - output_text += OPEN_FILES_REFERENCES_TEMPLATE.format( - references=" - " + usage_references) - return output_text + # Format into code block with syntax highlighting + if len(body) > 0: + return BODY_TEMPLATE.format( + content=CODE_TEMPLATE.format(lang="c++", code=body)) + else: + return "" def info_objc(cursor, cindex, settings): """Provide information about Objective C cursors.""" @@ -517,7 +588,6 @@ def pop_prepending_empty_lines(lines): break return lines[first_non_empty_line_idx:] - import string lines = raw_comment.split('\n') chars_to_strip = '/' + '*' + '!' + string.whitespace lines = [line.lstrip(chars_to_strip) for line in lines] @@ -534,6 +604,45 @@ def pop_prepending_empty_lines(lines): clean_lines.append(line) return '\n'.join(clean_lines) + @staticmethod + def doxygen_comment(mdcomment): + """Transform cleaned doxygen comment to valid markdown.""" + result = mdcomment + index = mdcomment.find("@param") + if (index >= 0): + result = result[:index] + "\n**Parameters**:\n" + result[index:] + doc_replace = [ + [r'@param\s+([_a-zA-Z0-9.]+)\s*', "- `\\1`: "], + [r'@(retval|returns?)\b\s*', "\n**Returns**:\n"], + [r'@(exception|throws?)\b\s*', "\n**Exceptions**:\n"], + [r'@(sa|see(also)?)\b\s*', "\n**See also**:\n"], + [r'@f\$', "`"], + [r'@[{}]', ""], + ] + for replace in doc_replace: + result = re.sub(replace[0], replace[1], result) + window = sublime.active_window() + + def _make_doxygen_hyperlink(match): + spelling = match.group(1) + if len(spelling) == 0: + return spelling + symbol = window.lookup_symbol_in_index(spelling) + if len(symbol) == 0: + return spelling + location_tuple = symbol[0] + location = IndexLocation(filename=location_tuple[0], + line=location_tuple[2][0], + column=location_tuple[2][1]) + link = Popup.link_from_location(location, spelling, + trailing_space=False) + return link + result = re.sub(r'\b([_a-zA-Z0-9]+)(?=\(\))', + _make_doxygen_hyperlink, result) + result = re.sub(r'#([_a-zA-Z0-9]+)\b', + _make_doxygen_hyperlink, result) + return result + @staticmethod def location_from_type(clang_type): """Return location from type. diff --git a/plugin/settings/settings_storage.py b/plugin/settings/settings_storage.py index 3c03df9b..b746e5ed 100644 --- a/plugin/settings/settings_storage.py +++ b/plugin/settings/settings_storage.py @@ -105,11 +105,13 @@ class SettingsStorage: "max_cache_age", "popup_maximum_height", "popup_maximum_width", + "popup_sections", "progress_style", "show_errors", - "show_index_references", "show_type_body", "show_type_info", + "show_index_references", + "show_doc_as_markdown", "target_compilers", "triggers", "use_default_definitions", diff --git a/tests/test_error_vis.py b/tests/test_error_vis.py index 9a16e6e3..aad4a1bb 100644 --- a/tests/test_error_vis.py +++ b/tests/test_error_vis.py @@ -265,6 +265,7 @@ def test_info_no_full(self): self.set_up_view(file_name) completer, settings = self.set_up_completer() settings.show_index_references = False + settings.show_doc_as_markdown = False # Check the current cursor position is completable. self.assertEqual(self.get_row(17), " MyCoolClass cool_class;") pos = self.view.text_point(17, 7) @@ -317,6 +318,7 @@ def test_info_full(self): self.set_up_view(file_name) completer, settings = self.set_up_completer() settings.show_index_references = False + settings.show_doc_as_markdown = False # Check the current cursor position is completable. self.assertEqual(self.get_row(18), " cool_class.foo(2, 2);") pos = self.view.text_point(18, 15) @@ -364,6 +366,7 @@ def test_info_arguments_link(self): self.set_up_view(file_name) completer, settings = self.set_up_completer() settings.show_index_references = False + settings.show_doc_as_markdown = False cursor_row_col = ZeroIndexedRowCol.from_one_indexed( OneIndexedRowCol(10, 15)) # Check the current cursor position is completable. From 14155625a540ee92fca30d0c1a9c5798a25619eb Mon Sep 17 00:00:00 2001 From: lexouduck Date: Tue, 28 Oct 2025 22:19:40 +0100 Subject: [PATCH 13/13] update: now opts-in to use python 3.8 --- .python-version | 1 + easy_clang_complete.sublime-project | 8 ++++++++ plugin/utils/module_reloader.py | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 .python-version diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..cc1923a4 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.8 diff --git a/easy_clang_complete.sublime-project b/easy_clang_complete.sublime-project index b3192c32..e73cbbf0 100644 --- a/easy_clang_complete.sublime-project +++ b/easy_clang_complete.sublime-project @@ -43,6 +43,14 @@ "sublack": { "black_on_save": false + }, + "LSP":{ + "LSP-pyright": + { + "settings": { + "pyright.dev_environment": "sublime_text_38" + } + } } } } diff --git a/plugin/utils/module_reloader.py b/plugin/utils/module_reloader.py index f5bf090a..c4b30227 100644 --- a/plugin/utils/module_reloader.py +++ b/plugin/utils/module_reloader.py @@ -32,7 +32,7 @@ def reload_once(prefix, ignore_string): """Reload all modules once.""" try_counter = 0 try: - for name, module in sys.modules.items(): + for name, module in sys.modules.copy().items(): if name.startswith(prefix) and ignore_string not in name: log.debug("Reloading module: '%s'", name) imp.reload(module)