From 629258241f3c81387d8f9b10b2fa2329773ec19e Mon Sep 17 00:00:00 2001 From: Aria Desires Date: Fri, 21 Nov 2025 10:47:38 -0500 Subject: [PATCH] [ty] Implement docstring rendering to markdown (#21550) ## Summary This introduces a very bad and naive python-docstring-flavoured-reStructuredText to github-flavor-markdown translator. The main goal is to try to preserve a lot of the formatting and plaintext, progressively enhance the content when we find things we know about, and escape the text when we find things that might get corrupt. Previously I'd broken this out into rendering each different format, but with this approach you don't really need to? ## Test Plan Lots of snapshot tests, also messed around in some random stdlib modules. --- crates/ty_ide/src/docstring.rs | 1007 +++++++++++++++++++++++++++----- crates/ty_ide/src/hover.rs | 122 +--- 2 files changed, 904 insertions(+), 225 deletions(-) diff --git a/crates/ty_ide/src/docstring.rs b/crates/ty_ide/src/docstring.rs index b749b1feb3..163dbe1ae2 100644 --- a/crates/ty_ide/src/docstring.rs +++ b/crates/ty_ide/src/docstring.rs @@ -65,11 +65,7 @@ impl Docstring { /// Render the docstring for markdown display pub fn render_markdown(&self) -> String { let trimmed = documentation_trim(&self.0); - // TODO: now actually parse it and "render" it to markdown. - // - // For now we just wrap the content in a plaintext codeblock - // to avoid the contents erroneously being interpreted as markdown. - format!("```text\n{trimmed}\n```") + render_markdown(&trimmed) } /// Extract parameter documentation from popular docstring formats. @@ -153,6 +149,223 @@ fn documentation_trim(docs: &str) -> String { output } +/// Given a presumed reStructuredText docstring, render it to GitHub Flavored Markdown. +/// +/// This function assumes the input has had its whitespace normalized by `documentation_trim`, +/// so leading whitespace is always a space, and newlines are always `\n`. +/// +/// The general approach here is: +/// +/// * Preserve the docstring verbatim by default, ensuring indent/linewraps are preserved +/// * Escape problematic things where necessary (bare `__dunder__` => `\_\_dunder\_\_`) +/// * Introduce code fences where appropriate +/// +/// The first rule is significant in ensuring various docstring idioms render clearly. +/// In particular ensuring things like this are faithfully rendered: +/// +/// ```text +/// param1 -- a good parameter +/// param2 -- another good parameter +/// with longer docs +/// ``` +/// +/// If we didn't go out of our way to preserve the indentation and line-breaks, markdown would +/// constantly render inputs like that into abominations like: +/// +/// ```html +///

+/// param1 -- a good parameter param2 -- another good parameter +///

+/// +/// +/// with longer docs +/// +/// ``` +fn render_markdown(docstring: &str) -> String { + // TODO: there is a convention that `singletick` is for items that can + // be looked up in-scope while ``multitick`` is for opaque inline code. + // While rendering this we should make note of all the `singletick` locations + // and (possibly in a higher up piece of logic) try to resolve the names for + // cross-linking. (Similar to `TypeDetails` in the type formatting code.) + let mut output = String::new(); + let mut first_line = true; + let mut block_indent = 0; + let mut in_doctest = false; + let mut starting_literal = false; + let mut in_literal = false; + let mut in_any_code = false; + for untrimmed_line in docstring.lines() { + // We can assume leading whitespace has been normalized + let mut line = untrimmed_line.trim_start_matches(' '); + let line_indent = untrimmed_line.len() - line.len(); + + // First thing's first, add a newline to start the new line + if !first_line { + // If we're not in a codeblock, add trailing space to the line to authentically wrap it + // (Lines ending with two spaces tell markdown to preserve a linebreak) + if !in_any_code { + output.push_str(" "); + } + // Only push newlines if we're not scanning for a real line + if !starting_literal { + output.push('\n'); + } + } + + // If we're in a literal block and we find a non-empty dedented line, end the block + // TODO: we should remove all the trailing blank lines + // (Just pop all trailing `\n` from `output`?) + if in_literal && line_indent < block_indent && !line.is_empty() { + in_literal = false; + in_any_code = false; + block_indent = 0; + output.push_str("```\n"); + } + + // We previously entered a literal block and we just found our first non-blank line + // So now we're actually in the literal block + if starting_literal && !line.is_empty() { + starting_literal = false; + in_literal = true; + in_any_code = true; + block_indent = line_indent; + // TODO: I hope people don't have literal blocks about markdown code fence syntax + // TODO: should we not be this aggressive? Let it autodetect? + // TODO: respect `.. code-block::` directives: + // + output.push_str("\n```python\n"); + } + + // If we're not in a codeblock and we see something that signals a doctest, start one + if !in_any_code && line.starts_with(">>>") { + block_indent = line_indent; + in_doctest = true; + in_any_code = true; + // TODO: is there something more specific? `pycon`? + output.push_str("```python\n"); + } + + // If we're not in a codeblock and we see something that signals a literal block, start one + if !in_any_code && let Some(without_lit) = line.strip_suffix("::") { + let trimmed_without_lit = without_lit.trim(); + if let Some(character) = trimmed_without_lit.chars().next_back() { + if character.is_whitespace() { + // Remove the marker completely + line = trimmed_without_lit; + } else { + // Only remove the first `:` + line = line.strip_suffix(":").unwrap(); + } + } else { + // Delete whole line + line = trimmed_without_lit; + } + starting_literal = true; + } + + // Add this line's indentation. + // We could subtract the block_indent here but in practice it's uglier + // TODO: should we not do this if the `line.is_empty()`? When would it matter? + for _ in 0..line_indent { + // If we're not in a codeblock use non-breaking spaces to preserve the indent + if !in_any_code { + // TODO: would the raw unicode codepoint be handled *better* or *worse* + // by various IDEs? VS Code handles this approach well, at least. + output.push_str(" "); + } else { + output.push(' '); + } + } + + if !in_any_code { + // This line is plain text, so we need to escape things that are inert in reST + // but active syntax in markdown... but not if it's inside `inline code`. + // Inline-code syntax is shared by reST and markdown which is really convenient + // except we need to find and parse it anyway to do this escaping properly! :( + // For now we assume `inline code` does not span a line (I'm not even sure if can). + // + // Things that need to be escaped: underscores + // + // e.g. we want __init__ => \_\_init\_\_ but `__init__` => `__init__` + let escape = |input: &str| input.replace('_', "\\_"); + + let mut in_inline_code = false; + let mut first_chunk = true; + let mut opening_tick_count = 0; + let mut current_tick_count = 0; + for chunk in line.split('`') { + // First chunk is definitionally not in inline-code and so always plaintext + if first_chunk { + first_chunk = false; + output.push_str(&escape(chunk)); + continue; + } + // Not in first chunk, emit the ` between the last chunk and this one + output.push('`'); + current_tick_count += 1; + + // If we're in an inline block and have enough close-ticks to terminate it, do so. + // TODO: we parse ``hello```there` as (hello)(there) which probably isn't correct + // (definitely not for markdown) but it's close enough for horse grenades in this + // MVP impl. Notably we're verbatime emitting all the `'s so as long as reST and + // markdown agree we're *fine*. The accuracy of this parsing only affects the + // accuracy of where we apply escaping (so we need to misparse and see escapables + // for any of this to matter). + if opening_tick_count > 0 && current_tick_count >= opening_tick_count { + opening_tick_count = 0; + current_tick_count = 0; + in_inline_code = false; + } + + // If this chunk is completely empty we're just in a run of ticks, continue + if chunk.is_empty() { + continue; + } + + // Ok the chunk is non-empty, our run of ticks is complete + if in_inline_code { + // The previous check for >= open_tick_count didn't trip, so these can't close + // and these ticks will be verbatim rendered in the content + current_tick_count = 0; + } else if current_tick_count > 0 { + // Ok we're now in inline code + opening_tick_count = current_tick_count; + current_tick_count = 0; + in_inline_code = true; + } + + // Finally include the content either escaped or not + if in_inline_code { + output.push_str(chunk); + } else { + output.push_str(&escape(chunk)); + } + } + // NOTE: explicitly not "flushing" the ticks here. + // We respect however the user closed their inline code. + } else if line.is_empty() { + if in_doctest { + // This is the end of a doctest + block_indent = 0; + in_any_code = false; + in_literal = false; + output.push_str("```"); + } + } else { + // Print the line verbatim, it's in code + output.push_str(line); + } + + first_line = false; + } + // Flush codeblock + if in_any_code { + output.push_str("\n```"); + } + + output +} + /// Extract parameter documentation from Google-style docstrings. fn extract_google_style_params(docstring: &str) -> HashMap { let mut param_docs = HashMap::new(); @@ -490,6 +703,511 @@ mod tests { use super::*; + // A nice doctest that is surrounded by prose + #[test] + fn dunder_escape() { + let docstring = r#" + Here _this_ and ___that__ should be escaped + Here *this* and **that** should be untouched + Here `this` and ``that`` should be untouched + + Here `_this_` and ``__that__`` should be untouched + Here `_this_` ``__that__`` should be untouched + `_this_too_should_be_untouched_` + + Here `_this_```__that__`` should be untouched but this_is_escaped + Here ``_this_```__that__` should be untouched but this_is_escaped + + Here `_this_ and _that_ should be escaped (but isn't) + Here _this_ and _that_` should be escaped + `Here _this_ and _that_ should be escaped (but isn't) + Here _this_ and _that_ should be escaped` + + Here ```_is_``__a__`_balanced_``_mess_``` + Here ```_is_`````__a__``_random_````_mess__```` + ```_is_`````__a__``_random_````_mess__```` + "#; + + let docstring = Docstring::new(docstring.to_owned()); + + assert_snapshot!(docstring.render_plaintext(), @r" + Here _this_ and ___that__ should be escaped + Here *this* and **that** should be untouched + Here `this` and ``that`` should be untouched + + Here `_this_` and ``__that__`` should be untouched + Here `_this_` ``__that__`` should be untouched + `_this_too_should_be_untouched_` + + Here `_this_```__that__`` should be untouched but this_is_escaped + Here ``_this_```__that__` should be untouched but this_is_escaped + + Here `_this_ and _that_ should be escaped (but isn't) + Here _this_ and _that_` should be escaped + `Here _this_ and _that_ should be escaped (but isn't) + Here _this_ and _that_ should be escaped` + + Here ```_is_``__a__`_balanced_``_mess_``` + Here ```_is_`````__a__``_random_````_mess__```` + ```_is_`````__a__``_random_````_mess__```` + "); + + assert_snapshot!(docstring.render_markdown(), @r" + Here \_this\_ and \_\_\_that\_\_ should be escaped + Here *this* and **that** should be untouched + Here `this` and ``that`` should be untouched + + Here `_this_` and ``__that__`` should be untouched + Here `_this_` ``__that__`` should be untouched + `_this_too_should_be_untouched_` + + Here `_this_```__that__`` should be untouched but this\_is\_escaped + Here ``_this_```__that__` should be untouched but this\_is\_escaped + + Here `_this_ and _that_ should be escaped (but isn't) + Here \_this\_ and \_that\_` should be escaped + `Here _this_ and _that_ should be escaped (but isn't) + Here \_this\_ and \_that\_ should be escaped` + + Here ```_is_``__a__`_balanced_``_mess_``` + Here ```_is_`````__a__``\_random\_````_mess__```` + ```_is_`````__a__``\_random\_````_mess__```` + "); + } + + // A literal block where the `::` is flush with the paragraph + // and should become `:` + #[test] + fn literal_colon() { + let docstring = r#" + Check out this great example code:: + + x_y = "hello" + + if len(x_y) > 4: + print(x_y) + else: + print("too short :(") + + print("done") + + You love to see it. + "#; + + let docstring = Docstring::new(docstring.to_owned()); + + assert_snapshot!(docstring.render_plaintext(), @r#" + Check out this great example code:: + + x_y = "hello" + + if len(x_y) > 4: + print(x_y) + else: + print("too short :(") + + print("done") + + You love to see it. + "#); + + assert_snapshot!(docstring.render_markdown(), @r#" + Check out this great example code: + ```python + x_y = "hello" + + if len(x_y) > 4: + print(x_y) + else: + print("too short :(") + + print("done") + + ``` + You love to see it. + "#); + } + + // A literal block where the `::` with the paragraph + // and should be erased + #[test] + fn literal_space() { + let docstring = r#" + Check out this great example code :: + + x_y = "hello" + + if len(x_y) > 4: + print(x_y) + else: + print("too short :(") + + print("done") + + You love to see it. + "#; + + let docstring = Docstring::new(docstring.to_owned()); + + assert_snapshot!(docstring.render_plaintext(), @r#" + Check out this great example code :: + + x_y = "hello" + + if len(x_y) > 4: + print(x_y) + else: + print("too short :(") + + print("done") + + You love to see it. + "#); + + assert_snapshot!(docstring.render_markdown(), @r#" + Check out this great example code : + ```python + x_y = "hello" + + if len(x_y) > 4: + print(x_y) + else: + print("too short :(") + + print("done") + + ``` + You love to see it. + "#); + } + + // A literal block where the `::` is floating + // and the whole line should be deleted + #[test] + fn literal_own_line() { + let docstring = r#" + Check out this great example code + :: + + x_y = "hello" + + if len(x_y) > 4: + print(x_y) + else: + print("too short :(") + + print("done") + + You love to see it. + "#; + + let docstring = Docstring::new(docstring.to_owned()); + + assert_snapshot!(docstring.render_plaintext(), @r#" + Check out this great example code + :: + + x_y = "hello" + + if len(x_y) > 4: + print(x_y) + else: + print("too short :(") + + print("done") + + You love to see it. + "#); + + assert_snapshot!(docstring.render_markdown(), @r#" + Check out this great example code +      + ```python + x_y = "hello" + + if len(x_y) > 4: + print(x_y) + else: + print("too short :(") + + print("done") + + ``` + You love to see it. + "#); + } + + // A literal block where the blank lines are missing + // and I have no idea what Should happen but let's record what Does + #[test] + fn literal_squeezed() { + let docstring = r#" + Check out this great example code:: + x_y = "hello" + + if len(x_y) > 4: + print(x_y) + else: + print("too short :(") + + print("done") + You love to see it. + "#; + + let docstring = Docstring::new(docstring.to_owned()); + + assert_snapshot!(docstring.render_plaintext(), @r#" + Check out this great example code:: + x_y = "hello" + + if len(x_y) > 4: + print(x_y) + else: + print("too short :(") + + print("done") + You love to see it. + "#); + + assert_snapshot!(docstring.render_markdown(), @r#" + Check out this great example code: + ```python + x_y = "hello" + + if len(x_y) > 4: + print(x_y) + else: + print("too short :(") + + print("done") + ``` + You love to see it. + "#); + } + + // A literal block where the docstring just ends + // and we should tidy up + #[test] + fn literal_flush() { + let docstring = r#" + Check out this great example code:: + + x_y = "hello" + + if len(x_y) > 4: + print(x_y) + else: + print("too short :(") + + print("done")"#; + + let docstring = Docstring::new(docstring.to_owned()); + + assert_snapshot!(docstring.render_plaintext(), @r#" + Check out this great example code:: + + x_y = "hello" + + if len(x_y) > 4: + print(x_y) + else: + print("too short :(") + + print("done") + "#); + + assert_snapshot!(docstring.render_markdown(), @r#" + Check out this great example code: + ```python + x_y = "hello" + + if len(x_y) > 4: + print(x_y) + else: + print("too short :(") + + print("done") + ``` + "#); + } + + // A nice doctest that is surrounded by prose + #[test] + fn doctest_simple() { + let docstring = r#" + This is a function description + + >>> thing.do_thing() + wow it did the thing + >>> thing.do_other_thing() + it sure did the thing + + As you can see it did the thing! + "#; + + let docstring = Docstring::new(docstring.to_owned()); + + assert_snapshot!(docstring.render_plaintext(), @r" + This is a function description + + >>> thing.do_thing() + wow it did the thing + >>> thing.do_other_thing() + it sure did the thing + + As you can see it did the thing! + "); + + assert_snapshot!(docstring.render_markdown(), @r" + This is a function description + + ```python + >>> thing.do_thing() + wow it did the thing + >>> thing.do_other_thing() + it sure did the thing + ``` + As you can see it did the thing! + "); + } + + // A nice doctest that is surrounded by prose with an indent + #[test] + fn doctest_simple_indent() { + let docstring = r#" + This is a function description + + >>> thing.do_thing() + wow it did the thing + >>> thing.do_other_thing() + it sure did the thing + + As you can see it did the thing! + "#; + + let docstring = Docstring::new(docstring.to_owned()); + + assert_snapshot!(docstring.render_plaintext(), @r" + This is a function description + + >>> thing.do_thing() + wow it did the thing + >>> thing.do_other_thing() + it sure did the thing + + As you can see it did the thing! + "); + + assert_snapshot!(docstring.render_markdown(), @r" + This is a function description + + ```python + >>> thing.do_thing() + wow it did the thing + >>> thing.do_other_thing() + it sure did the thing + ``` + As you can see it did the thing! + "); + } + + // A doctest that has nothing around it + #[test] + fn doctest_flush() { + let docstring = r#">>> thing.do_thing() + wow it did the thing + >>> thing.do_other_thing() + it sure did the thing"#; + + let docstring = Docstring::new(docstring.to_owned()); + + assert_snapshot!(docstring.render_plaintext(), @r" + >>> thing.do_thing() + wow it did the thing + >>> thing.do_other_thing() + it sure did the thing + "); + + assert_snapshot!(docstring.render_markdown(), @r" + ```python + >>> thing.do_thing() + wow it did the thing + >>> thing.do_other_thing() + it sure did the thing + ``` + "); + } + + // A doctest embedded in a literal block (it's just a literal block) + #[test] + fn literal_doctest() { + let docstring = r#" + This is a function description:: + + >>> thing.do_thing() + wow it did the thing + >>> thing.do_other_thing() + it sure did the thing + + As you can see it did the thing! + "#; + + let docstring = Docstring::new(docstring.to_owned()); + + assert_snapshot!(docstring.render_plaintext(), @r" + This is a function description:: + + >>> thing.do_thing() + wow it did the thing + >>> thing.do_other_thing() + it sure did the thing + + As you can see it did the thing! + "); + + assert_snapshot!(docstring.render_markdown(), @r" + This is a function description: + ```python + >>> thing.do_thing() + wow it did the thing + >>> thing.do_other_thing() + it sure did the thing + + ``` + As you can see it did the thing! + "); + } + + #[test] + fn doctest_indent_flush() { + let docstring = r#" + And so you can see that + >>> thing.do_thing() + wow it did the thing + >>> thing.do_other_thing() + it sure did the thing"#; + + let docstring = Docstring::new(docstring.to_owned()); + + assert_snapshot!(docstring.render_plaintext(), @r" + And so you can see that + >>> thing.do_thing() + wow it did the thing + >>> thing.do_other_thing() + it sure did the thing + "); + + assert_snapshot!(docstring.render_markdown(), @r" + And so you can see that + ```python + >>> thing.do_thing() + wow it did the thing + >>> thing.do_other_thing() + it sure did the thing + ``` + "); + } + #[test] fn test_google_style_parameter_documentation() { let docstring = r#" @@ -530,19 +1248,16 @@ mod tests { "); assert_snapshot!(docstring.render_markdown(), @r" - ```text - This is a function description. - - Args: - param1 (str): The first parameter description - param2 (int): The second parameter description - This is a continuation of param2 description. - param3: A parameter without type annotation - - Returns: - str: The return value description - - ``` + This is a function description. + + Args: +     param1 (str): The first parameter description +     param2 (int): The second parameter description +         This is a continuation of param2 description. +     param3: A parameter without type annotation + + Returns: +     str: The return value description "); } @@ -604,24 +1319,77 @@ mod tests { "); assert_snapshot!(docstring.render_markdown(), @r" - ```text - This is a function description. + This is a function description. + + Parameters + ---------- + param1 : str +     The first parameter description + param2 : int +     The second parameter description +     This is a continuation of param2 description. + param3 +     A parameter without type annotation + + Returns + ------- + str +     The return value description + "); + } - Parameters - ---------- - param1 : str - The first parameter description - param2 : int - The second parameter description - This is a continuation of param2 description. - param3 - A parameter without type annotation + #[test] + fn test_pep257_style_parameter_documentation() { + let docstring = r#"Insert an entry into the list of warnings filters (at the front). - Returns - ------- - str - The return value description + 'param1' -- The first parameter description + 'param2' -- The second parameter description + This is a continuation of param2 description. + 'param3' -- A parameter without type annotation + >>> print repr(foo.__doc__) + '\n This is the second line of the docstring.\n ' + >>> foo.__doc__.splitlines() + ['', ' This is the second line of the docstring.', ' '] + >>> trim(foo.__doc__) + 'This is the second line of the docstring.' + "#; + + let docstring = Docstring::new(docstring.to_owned()); + let param_docs = docstring.parameter_documentation(); + assert!(param_docs.is_empty()); + + assert_snapshot!(docstring.render_plaintext(), @r" + Insert an entry into the list of warnings filters (at the front). + + 'param1' -- The first parameter description + 'param2' -- The second parameter description + This is a continuation of param2 description. + 'param3' -- A parameter without type annotation + + >>> print repr(foo.__doc__) + '\n This is the second line of the docstring.\n ' + >>> foo.__doc__.splitlines() + ['', ' This is the second line of the docstring.', ' '] + >>> trim(foo.__doc__) + 'This is the second line of the docstring.' + "); + + assert_snapshot!(docstring.render_markdown(), @r" + Insert an entry into the list of warnings filters (at the front). + + 'param1' -- The first parameter description + 'param2' -- The second parameter description +             This is a continuation of param2 description. + 'param3' -- A parameter without type annotation + + ```python + >>> print repr(foo.__doc__) + '\n This is the second line of the docstring.\n ' + >>> foo.__doc__.splitlines() + ['', ' This is the second line of the docstring.', ' '] + >>> trim(foo.__doc__) + 'This is the second line of the docstring.' ``` "); } @@ -638,12 +1406,7 @@ mod tests { assert_snapshot!(docstring.render_plaintext(), @"This is a simple function description without parameter documentation."); - assert_snapshot!(docstring.render_markdown(), @r" - ```text - This is a simple function description without parameter documentation. - - ``` - "); + assert_snapshot!(docstring.render_markdown(), @"This is a simple function description without parameter documentation."); } #[test] @@ -692,19 +1455,16 @@ mod tests { "); assert_snapshot!(docstring.render_markdown(), @r" - ```text - This is a function description. - - Args: - param1 (str): Google-style parameter - param2 (int): Another Google-style parameter - - Parameters - ---------- - param3 : bool - NumPy-style parameter - - ``` + This is a function description. + + Args: +     param1 (str): Google-style parameter +     param2 (int): Another Google-style parameter + + Parameters + ---------- + param3 : bool +     NumPy-style parameter "); } @@ -750,17 +1510,14 @@ mod tests { "); assert_snapshot!(docstring.render_markdown(), @r" - ```text - This is a function description. - - :param str param1: The first parameter description - :param int param2: The second parameter description - This is a continuation of param2 description. - :param param3: A parameter without type annotation - :returns: The return value description + This is a function description. + + :param str param1: The first parameter description + :param int param2: The second parameter description +     This is a continuation of param2 description. + :param param3: A parameter without type annotation + :returns: The return value description :rtype: str - - ``` "); } @@ -818,21 +1575,18 @@ mod tests { "); assert_snapshot!(docstring.render_markdown(), @r" - ```text - This is a function description. - - Args: - param1 (str): Google-style parameter - - :param int param2: reST-style parameter - :param param3: Another reST-style parameter - - Parameters - ---------- - param4 : bool - NumPy-style parameter - - ``` + This is a function description. + + Args: +     param1 (str): Google-style parameter + + :param int param2: reST-style parameter + :param param3: Another reST-style parameter + + Parameters + ---------- + param4 : bool +     NumPy-style parameter "); } @@ -894,25 +1648,22 @@ mod tests { "); assert_snapshot!(docstring.render_markdown(), @r" - ```text - This is a function description. - - Parameters - ---------- - param1 : str - The first parameter description - param2 : int - The second parameter description - This is a continuation of param2 description. - param3 - A parameter without type annotation - - Returns - ------- - str - The return value description - - ``` + This is a function description. + + Parameters + ---------- + param1 : str +     The first parameter description + param2 : int +     The second parameter description +     This is a continuation of param2 description. + param3 +     A parameter without type annotation + + Returns + ------- + str +     The return value description "); } @@ -965,20 +1716,17 @@ mod tests { "); assert_snapshot!(docstring.render_markdown(), @r" - ```text - This is a function description. - - Parameters - ---------- - param1 : str - The first parameter description - param2 : int - The second parameter description - This is a continuation of param2 description. - param3 - A parameter without type annotation - - ``` + This is a function description. + + Parameters + ---------- + param1 : str +         The first parameter description + param2 : int +         The second parameter description +         This is a continuation of param2 description. + param3 +         A parameter without type annotation "); } @@ -1028,14 +1776,11 @@ mod tests { "); assert_snapshot!(docstring_windows.render_markdown(), @r" - ```text - This is a function description. - - Args: - param1 (str): The first parameter - param2 (int): The second parameter - - ``` + This is a function description. + + Args: +     param1 (str): The first parameter +     param2 (int): The second parameter "); assert_snapshot!(docstring_mac.render_plaintext(), @r" @@ -1047,14 +1792,11 @@ mod tests { "); assert_snapshot!(docstring_mac.render_markdown(), @r" - ```text - This is a function description. - - Args: - param1 (str): The first parameter - param2 (int): The second parameter - - ``` + This is a function description. + + Args: +     param1 (str): The first parameter +     param2 (int): The second parameter "); assert_snapshot!(docstring_unix.render_plaintext(), @r" @@ -1066,14 +1808,11 @@ mod tests { "); assert_snapshot!(docstring_unix.render_markdown(), @r" - ```text - This is a function description. - - Args: - param1 (str): The first parameter - param2 (int): The second parameter - - ``` + This is a function description. + + Args: +     param1 (str): The first parameter +     param2 (int): The second parameter "); } } diff --git a/crates/ty_ide/src/hover.rs b/crates/ty_ide/src/hover.rs index f1de7316bd..1b8716f18e 100644 --- a/crates/ty_ide/src/hover.rs +++ b/crates/ty_ide/src/hover.rs @@ -245,14 +245,11 @@ mod tests { ) -> Unknown ``` --- - ```text - This is such a great func!! - - Args: - a: first for a reason - b: coming for `a`'s title - - ``` + This is such a great func!! + + Args: +     a: first for a reason +     b: coming for `a`'s title --------------------------------------------- info[hover]: Hovered content is --> main.py:11:1 @@ -303,14 +300,11 @@ mod tests { ) -> Unknown ``` --- - ```text - This is such a great func!! - - Args: - a: first for a reason - b: coming for `a`'s title - - ``` + This is such a great func!! + + Args: +     a: first for a reason +     b: coming for `a`'s title --------------------------------------------- info[hover]: Hovered content is --> main.py:2:5 @@ -369,14 +363,11 @@ mod tests { ``` --- - ```text - This is such a great class!! - - Don't you know? - + This is such a great class!! + +     Don't you know? + Everyone loves my class!! - - ``` --------------------------------------------- info[hover]: Hovered content is --> main.py:24:1 @@ -434,14 +425,11 @@ mod tests { ``` --- - ```text - This is such a great class!! - - Don't you know? - + This is such a great class!! + +     Don't you know? + Everyone loves my class!! - - ``` --------------------------------------------- info[hover]: Hovered content is --> main.py:2:7 @@ -497,10 +485,7 @@ mod tests { ``` --- - ```text initializes MyClass (perfectly) - - ``` --------------------------------------------- info[hover]: Hovered content is --> main.py:24:5 @@ -556,10 +541,7 @@ mod tests { ``` --- - ```text initializes MyClass (perfectly) - - ``` --------------------------------------------- info[hover]: Hovered content is --> main.py:4:11 @@ -618,14 +600,11 @@ mod tests { ``` --- - ```text - This is such a great class!! - - Don't you know? - + This is such a great class!! + +     Don't you know? + Everyone loves my class!! - - ``` --------------------------------------------- info[hover]: Hovered content is --> main.py:23:5 @@ -692,14 +671,11 @@ mod tests { ) -> Unknown ``` --- - ```text - This is such a great func!! - - Args: - a: first for a reason - b: coming for `a`'s title - - ``` + This is such a great func!! + + Args: +     a: first for a reason +     b: coming for `a`'s title --------------------------------------------- info[hover]: Hovered content is --> main.py:25:3 @@ -973,10 +949,7 @@ def ab(a: str): ... (a: int) -> Unknown ``` --- - ```text the int overload - - ``` --------------------------------------------- info[hover]: Hovered content is --> main.py:4:1 @@ -1036,10 +1009,7 @@ def ab(a: str): (a: str) -> Unknown ``` --- - ```text the int overload - - ``` --------------------------------------------- info[hover]: Hovered content is --> main.py:4:1 @@ -1105,10 +1075,7 @@ def ab(a: int): ) -> Unknown ``` --- - ```text the two arg overload - - ``` --------------------------------------------- info[hover]: Hovered content is --> main.py:4:1 @@ -1168,10 +1135,7 @@ def ab(a: int): (a: int) -> Unknown ``` --- - ```text the two arg overload - - ``` --------------------------------------------- info[hover]: Hovered content is --> main.py:4:1 @@ -1243,10 +1207,7 @@ def ab(a: int, *, c: int): ) -> Unknown ``` --- - ```text keywordless overload - - ``` --------------------------------------------- info[hover]: Hovered content is --> main.py:4:1 @@ -1318,10 +1279,7 @@ def ab(a: int, *, c: int): ) -> Unknown ``` --- - ```text keywordless overload - - ``` --------------------------------------------- info[hover]: Hovered content is --> main.py:4:1 @@ -1386,10 +1344,7 @@ def ab(a: int, *, c: int): ) -> Unknown ``` --- - ```text The first overload - - ``` --------------------------------------------- info[hover]: Hovered content is --> main.py:19:1 @@ -1441,10 +1396,7 @@ def ab(a: int, *, c: int): (a: str) -> Unknown ``` --- - ```text The first overload - - ``` --------------------------------------------- info[hover]: Hovered content is --> main.py:19:1 @@ -1494,12 +1446,9 @@ def ab(a: int, *, c: int): ``` --- - ```text - The cool lib_py module! - + The cool lib/_py module! + Wow this module rocks. - - ``` --------------------------------------------- info[hover]: Hovered content is --> main.py:4:1 @@ -1544,12 +1493,9 @@ def ab(a: int, *, c: int): Wow this module rocks. --------------------------------------------- - ```text - The cool lib_py module! - + The cool lib/_py module! + Wow this module rocks. - - ``` --------------------------------------------- info[hover]: Hovered content is --> main.py:2:8 @@ -2499,10 +2445,7 @@ def ab(a: int, *, c: int): bound method int.__add__(value: int, /) -> int ``` --- - ```text Return self+value. - - ``` --------------------------------------------- info[hover]: Hovered content is --> main.py:2:12 @@ -2618,10 +2561,7 @@ def ab(a: int, *, c: int): int | float ``` --- - ```text Convert a string or number to a floating-point number, if possible. - - ``` --------------------------------------------- info[hover]: Hovered content is --> main.py:2:4