src: De-indent docstrings passed to command/option/mapping definitions

This commit implements formatting behaviour when the first character of a
docstring is a newline. In that case, the exact indentation level of the
next line will be removed from that line and all subsequent non-empty lines.

An error will be returned if a subsequent non-empty line does not have the
same indentation level.

The docstrings are always trimmed (surrounding whitespaces) whether the
first character is a newline or not, as was the case prior to this commit.

Example: the following declaration

```
define-command test -docstring %{
    test: do something

    Nothing really.
        More
            indented
                lines.
} nop
```

would be rendered as

```
test: do something

Nothing really.
    More
        indented
            lines.
```

Related to #2405
This commit is contained in:
Frank LENORMAND 2019-07-23 13:09:41 +03:00
parent e42c81c8eb
commit da2f6c296a
3 changed files with 43 additions and 4 deletions

View File

@ -1163,9 +1163,9 @@ void define_command(const ParametersParser& parser, Context& context, const Shel
};
}
auto docstring = trim_whitespaces(parser.get_switch("docstring").value_or(StringView{}));
auto docstring = trim_indent(parser.get_switch("docstring").value_or(StringView{}));
cm.register_command(cmd_name, cmd, docstring.str(), desc, flags, CommandHelper{}, completer);
cm.register_command(cmd_name, cmd, docstring, desc, flags, CommandHelper{}, completer);
}
const CommandDesc define_command_cmd = {
@ -1590,7 +1590,7 @@ const CommandDesc declare_option_cmd = {
if (parser.get_switch("hidden"))
flags = OptionFlags::Hidden;
auto docstring = trim_whitespaces(parser.get_switch("docstring").value_or(StringView{})).str();
auto docstring = trim_indent(parser.get_switch("docstring").value_or(StringView{}));
OptionsRegistry& reg = GlobalScope::instance().option_registry();
@ -1672,7 +1672,7 @@ const CommandDesc map_key_cmd = {
KeyList mapping = parse_keys(parser[3]);
keymaps.map_key(key[0], keymap_mode, std::move(mapping),
trim_whitespaces(parser.get_switch("docstring").value_or("")).str());
trim_indent(parser.get_switch("docstring").value_or("")));
}
};

View File

@ -19,6 +19,36 @@ StringView trim_whitespaces(StringView str)
return {beg, end};
}
String trim_indent(StringView str)
{
if (str.empty())
return {};
else if (str[0_byte] != '\n')
return trim_whitespaces(str).str();
str = str.substr(1_byte);
const CharCount docstring_length = str.char_length();
CharCount level_indent = 0;
while (level_indent < docstring_length
and is_horizontal_blank(str[level_indent]))
level_indent++;
if (level_indent >= docstring_length or not level_indent)
return trim_whitespaces(str).str();
const auto str_indent = str.substr(0, level_indent);
auto s = str | split<StringView>('\n') | transform([&](auto&& line) {
if (line.empty())
return line;
else if (not prefix_match(line, str_indent))
throw runtime_error("inconsistent indentation in the string");
return line.substr(str_indent.char_length());
});
return trim_whitespaces(join(s, '\n', false)).str();
}
String escape(StringView str, StringView characters, char escape)
{
@ -379,6 +409,14 @@ UnitTest test_string{[]()
kak_assert(wrapped2[1] == "unknown");
kak_assert(wrapped2[2] == "type");
kak_assert(trim_indent(" ") == "");
kak_assert(trim_indent("no-indent") == "no-indent");
kak_assert(trim_indent("\nno-indent") == "no-indent");
kak_assert(trim_indent("\n indent\n indent") == "indent\nindent");
kak_assert(trim_indent("\n indent\n indent") == "indent\n indent");
kak_expect_throw(runtime_error, trim_indent("\n indent\nno-indent"));
kak_assert(escape(R"(\youpi:matin:tchou\:)", ":\\", '\\') == R"(\\youpi\:matin\:tchou\\\:)");
kak_assert(unescape(R"(\\youpi\:matin\:tchou\\\:)", ":\\", '\\') == R"(\youpi:matin:tchou\:)");

View File

@ -10,6 +10,7 @@ namespace Kakoune
{
StringView trim_whitespaces(StringView str);
String trim_indent(StringView str);
String escape(StringView str, StringView characters, char escape);
String unescape(StringView str, StringView characters, char escape);