Forráskód Böngészése

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)
Ali Mohammad Pur 3 éve
szülő
commit
a76730823a
3 módosított fájl, 141 hozzáadás és 59 törlés
  1. 67 59
      Userland/Shell/Shell.cpp
  2. 1 0
      Userland/Shell/Shell.h
  3. 73 0
      Userland/Shell/main.cpp

+ 67 - 59
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)

+ 1 - 0
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,

+ 73 - 0
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;