diff options
author | Ali Mohammad Pur <ali.mpfard@gmail.com> | 2022-03-06 13:04:31 +0330 |
---|---|---|
committer | Andreas Kling <kling@serenityos.org> | 2022-03-06 13:20:41 +0100 |
commit | a76730823ab3b7e125ea501e095a3ffd25a7dd18 (patch) | |
tree | 133fe8212d15532bd2888f4cdcc3c68b42662362 /Userland/Shell | |
parent | c4d937747751d59ca3a5a1c04f608a0ccb2ee9b0 (diff) | |
download | serenity-a76730823ab3b7e125ea501e095a3ffd25a7dd18.zip |
Shell: Be more smart with pasted stuff
Shell can now use LibLine's `on_paste` hook to more intelligently escape
pasted data, with the following heuristics:
- If the current command is invalid, just pile the pasted string on top
- If the cursor is *after* a command node, escape the pasted data,
whichever way yields a smaller encoding
- If the cursor is at the start of or in the middle of a command name,
paste the data as-is, assuming that the user wants to paste code
- If the cursor is otherwise in some argument, escape the pasted data
according to which kind of string the cursor is in the middle of
(double-quoted, single-quoted or a simple bareword)
Diffstat (limited to 'Userland/Shell')
-rw-r--r-- | Userland/Shell/Shell.cpp | 126 | ||||
-rw-r--r-- | Userland/Shell/Shell.h | 1 | ||||
-rw-r--r-- | Userland/Shell/main.cpp | 73 |
3 files changed, 141 insertions, 59 deletions
diff --git a/Userland/Shell/Shell.cpp b/Userland/Shell/Shell.cpp index d66cf03b74..6155a0f794 100644 --- a/Userland/Shell/Shell.cpp +++ b/Userland/Shell/Shell.cpp @@ -1168,6 +1168,8 @@ Shell::SpecialCharacterEscapeMode Shell::special_character_escape_mode(u32 code_ case '}': case '&': case ';': + case '?': + case '*': case ' ': if (mode == EscapeMode::SingleQuotedString || mode == EscapeMode::DoubleQuotedString) return SpecialCharacterEscapeMode::Untouched; @@ -1184,76 +1186,82 @@ Shell::SpecialCharacterEscapeMode Shell::special_character_escape_mode(u32 code_ } } -String Shell::escape_token(StringView token, EscapeMode escape_mode) +static String do_escape(Shell::EscapeMode escape_mode, auto& token) { - auto do_escape = [escape_mode](auto& token) { - StringBuilder builder; - for (auto c : token) { - static_assert(sizeof(c) == sizeof(u32) || sizeof(c) == sizeof(u8)); - switch (special_character_escape_mode(c, escape_mode)) { - case SpecialCharacterEscapeMode::Untouched: - if constexpr (sizeof(c) == sizeof(u8)) - builder.append(c); - else - builder.append(Utf32View { &c, 1 }); - break; - case SpecialCharacterEscapeMode::Escaped: - if (escape_mode == EscapeMode::SingleQuotedString) - builder.append("'"); - builder.append('\\'); + StringBuilder builder; + for (auto c : token) { + static_assert(sizeof(c) == sizeof(u32) || sizeof(c) == sizeof(u8)); + switch (Shell::special_character_escape_mode(c, escape_mode)) { + case Shell::SpecialCharacterEscapeMode::Untouched: + if constexpr (sizeof(c) == sizeof(u8)) builder.append(c); - if (escape_mode == EscapeMode::SingleQuotedString) - builder.append("'"); + else + builder.append(Utf32View { &c, 1 }); + break; + case Shell::SpecialCharacterEscapeMode::Escaped: + if (escape_mode == Shell::EscapeMode::SingleQuotedString) + builder.append("'"); + builder.append('\\'); + builder.append(c); + if (escape_mode == Shell::EscapeMode::SingleQuotedString) + builder.append("'"); + break; + case Shell::SpecialCharacterEscapeMode::QuotedAsEscape: + if (escape_mode == Shell::EscapeMode::SingleQuotedString) + builder.append("'"); + if (escape_mode != Shell::EscapeMode::DoubleQuotedString) + builder.append("\""); + switch (c) { + case '\n': + builder.append(R"(\n)"); break; - case SpecialCharacterEscapeMode::QuotedAsEscape: - if (escape_mode == EscapeMode::SingleQuotedString) - builder.append("'"); - if (escape_mode != EscapeMode::DoubleQuotedString) - builder.append("\""); - switch (c) { - case '\n': - builder.append(R"(\n)"); - break; - case '\t': - builder.append(R"(\t)"); - break; - case '\r': - builder.append(R"(\r)"); - break; - default: - VERIFY_NOT_REACHED(); - } - if (escape_mode != EscapeMode::DoubleQuotedString) - builder.append("\""); - if (escape_mode == EscapeMode::SingleQuotedString) - builder.append("'"); + case '\t': + builder.append(R"(\t)"); break; - case SpecialCharacterEscapeMode::QuotedAsHex: - if (escape_mode == EscapeMode::SingleQuotedString) - builder.append("'"); - if (escape_mode != EscapeMode::DoubleQuotedString) - builder.append("\""); - - if (c <= NumericLimits<u8>::max()) - builder.appendff(R"(\x{:0>2x})", static_cast<u8>(c)); - else - builder.appendff(R"(\u{:0>8x})", static_cast<u32>(c)); - - if (escape_mode != EscapeMode::DoubleQuotedString) - builder.append("\""); - if (escape_mode == EscapeMode::SingleQuotedString) - builder.append("'"); + case '\r': + builder.append(R"(\r)"); break; + default: + VERIFY_NOT_REACHED(); } + if (escape_mode != Shell::EscapeMode::DoubleQuotedString) + builder.append("\""); + if (escape_mode == Shell::EscapeMode::SingleQuotedString) + builder.append("'"); + break; + case Shell::SpecialCharacterEscapeMode::QuotedAsHex: + if (escape_mode == Shell::EscapeMode::SingleQuotedString) + builder.append("'"); + if (escape_mode != Shell::EscapeMode::DoubleQuotedString) + builder.append("\""); + + if (c <= NumericLimits<u8>::max()) + builder.appendff(R"(\x{:0>2x})", static_cast<u8>(c)); + else + builder.appendff(R"(\u{:0>8x})", static_cast<u32>(c)); + + if (escape_mode != Shell::EscapeMode::DoubleQuotedString) + builder.append("\""); + if (escape_mode == Shell::EscapeMode::SingleQuotedString) + builder.append("'"); + break; } + } - return builder.build(); - }; + return builder.build(); +} +String Shell::escape_token(Utf32View token, EscapeMode escape_mode) +{ + return do_escape(escape_mode, token); +} + +String Shell::escape_token(StringView token, EscapeMode escape_mode) +{ Utf8View view { token }; if (view.validate()) - return do_escape(view); - return do_escape(token); + return do_escape(escape_mode, view); + return do_escape(escape_mode, token); } String Shell::unescape_token(StringView token) diff --git a/Userland/Shell/Shell.h b/Userland/Shell/Shell.h index 3e43a03894..79a984c779 100644 --- a/Userland/Shell/Shell.h +++ b/Userland/Shell/Shell.h @@ -164,6 +164,7 @@ public: static String escape_token_for_double_quotes(StringView token); static String escape_token_for_single_quotes(StringView token); static String escape_token(StringView token, EscapeMode = EscapeMode::Bareword); + static String escape_token(Utf32View token, EscapeMode = EscapeMode::Bareword); static String unescape_token(StringView token); enum class SpecialCharacterEscapeMode { Untouched, diff --git a/Userland/Shell/main.cpp b/Userland/Shell/main.cpp index bcca36f59d..8ce039b4b7 100644 --- a/Userland/Shell/main.cpp +++ b/Userland/Shell/main.cpp @@ -82,6 +82,79 @@ ErrorOr<int> serenity_main(Main::Arguments arguments) editor->on_tab_complete = [&](const Line::Editor&) { return shell->complete(); }; + editor->on_paste = [&](Utf32View data, Line::Editor& editor) { + auto line = editor.line(editor.cursor()); + Shell::Parser parser(line, false); + auto ast = parser.parse(); + if (!ast) { + editor.insert(data); + return; + } + + auto hit_test_result = ast->hit_test_position(editor.cursor()); + // If the argument isn't meant to be an entire command, escape it. + // This allows copy-pasting entire commands where commands are expected, and otherwise escapes everything. + auto should_escape = false; + if (!hit_test_result.matching_node && hit_test_result.closest_command_node) { + // There's *some* command, but our cursor is immediate after it + should_escape = editor.cursor() >= hit_test_result.closest_command_node->position().end_offset; + hit_test_result.matching_node = hit_test_result.closest_command_node; + } else if (hit_test_result.matching_node && hit_test_result.closest_command_node) { + // There's a command, and we're at the end of or in the middle of some node. + auto leftmost_literal = hit_test_result.closest_command_node->leftmost_trivial_literal(); + if (leftmost_literal) + should_escape = !hit_test_result.matching_node->position().contains(leftmost_literal->position().start_offset); + } + + if (should_escape) { + String escaped_string; + Optional<char> trivia {}; + bool starting_trivia_already_provided = false; + auto escape_mode = Shell::Shell::EscapeMode::Bareword; + if (hit_test_result.matching_node->kind() == Shell::AST::Node::Kind::StringLiteral) { + // If we're pasting in a string literal, make sure to only consider that specific escape mode + auto* node = static_cast<Shell::AST::StringLiteral const*>(hit_test_result.matching_node.ptr()); + switch (node->enclosure_type()) { + case Shell::AST::StringLiteral::EnclosureType::None: + break; + case Shell::AST::StringLiteral::EnclosureType::SingleQuotes: + escape_mode = Shell::Shell::EscapeMode::SingleQuotedString; + trivia = '\''; + starting_trivia_already_provided = true; + break; + case Shell::AST::StringLiteral::EnclosureType::DoubleQuotes: + escape_mode = Shell::Shell::EscapeMode::DoubleQuotedString; + trivia = '"'; + starting_trivia_already_provided = true; + break; + } + } + + if (starting_trivia_already_provided) { + escaped_string = shell->escape_token(data, escape_mode); + } else { + escaped_string = shell->escape_token(data, Shell::Shell::EscapeMode::Bareword); + if (auto string = shell->escape_token(data, Shell::Shell::EscapeMode::SingleQuotedString); string.length() + 2 < escaped_string.length()) { + escaped_string = move(string); + trivia = '\''; + } + if (auto string = shell->escape_token(data, Shell::Shell::EscapeMode::DoubleQuotedString); string.length() + 2 < escaped_string.length()) { + escaped_string = move(string); + trivia = '"'; + } + } + + if (trivia.has_value() && !starting_trivia_already_provided) + editor.insert(*trivia); + + editor.insert(escaped_string); + + if (trivia.has_value()) + editor.insert(*trivia); + } else { + editor.insert(data); + } + }; }; const char* command_to_run = nullptr; |