mirror of
https://github.com/RGBCube/serenity
synced 2025-05-31 21:38:10 +00:00
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)
This commit is contained in:
parent
c4d9377477
commit
a76730823a
3 changed files with 148 additions and 66 deletions
|
@ -1168,6 +1168,8 @@ Shell::SpecialCharacterEscapeMode Shell::special_character_escape_mode(u32 code_
|
||||||
case '}':
|
case '}':
|
||||||
case '&':
|
case '&':
|
||||||
case ';':
|
case ';':
|
||||||
|
case '?':
|
||||||
|
case '*':
|
||||||
case ' ':
|
case ' ':
|
||||||
if (mode == EscapeMode::SingleQuotedString || mode == EscapeMode::DoubleQuotedString)
|
if (mode == EscapeMode::SingleQuotedString || mode == EscapeMode::DoubleQuotedString)
|
||||||
return SpecialCharacterEscapeMode::Untouched;
|
return SpecialCharacterEscapeMode::Untouched;
|
||||||
|
@ -1184,76 +1186,82 @@ Shell::SpecialCharacterEscapeMode Shell::special_character_escape_mode(u32 code_
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String do_escape(Shell::EscapeMode escape_mode, auto& token)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
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 '\t':
|
||||||
|
builder.append(R"(\t)");
|
||||||
|
break;
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
String Shell::escape_token(Utf32View token, EscapeMode escape_mode)
|
||||||
|
{
|
||||||
|
return do_escape(escape_mode, token);
|
||||||
|
}
|
||||||
|
|
||||||
String Shell::escape_token(StringView token, EscapeMode escape_mode)
|
String Shell::escape_token(StringView token, EscapeMode escape_mode)
|
||||||
{
|
{
|
||||||
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('\\');
|
|
||||||
builder.append(c);
|
|
||||||
if (escape_mode == EscapeMode::SingleQuotedString)
|
|
||||||
builder.append("'");
|
|
||||||
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("'");
|
|
||||||
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("'");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return builder.build();
|
|
||||||
};
|
|
||||||
|
|
||||||
Utf8View view { token };
|
Utf8View view { token };
|
||||||
if (view.validate())
|
if (view.validate())
|
||||||
return do_escape(view);
|
return do_escape(escape_mode, view);
|
||||||
return do_escape(token);
|
return do_escape(escape_mode, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
String Shell::unescape_token(StringView token)
|
String Shell::unescape_token(StringView token)
|
||||||
|
|
|
@ -164,6 +164,7 @@ public:
|
||||||
static String escape_token_for_double_quotes(StringView token);
|
static String escape_token_for_double_quotes(StringView token);
|
||||||
static String escape_token_for_single_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(StringView token, EscapeMode = EscapeMode::Bareword);
|
||||||
|
static String escape_token(Utf32View token, EscapeMode = EscapeMode::Bareword);
|
||||||
static String unescape_token(StringView token);
|
static String unescape_token(StringView token);
|
||||||
enum class SpecialCharacterEscapeMode {
|
enum class SpecialCharacterEscapeMode {
|
||||||
Untouched,
|
Untouched,
|
||||||
|
|
|
@ -82,6 +82,79 @@ ErrorOr<int> serenity_main(Main::Arguments arguments)
|
||||||
editor->on_tab_complete = [&](const Line::Editor&) {
|
editor->on_tab_complete = [&](const Line::Editor&) {
|
||||||
return shell->complete();
|
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;
|
const char* command_to_run = nullptr;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue