diff --git a/doc_src/cmds/history.rst b/doc_src/cmds/history.rst index f93e911d5..79e58b08a 100644 --- a/doc_src/cmds/history.rst +++ b/doc_src/cmds/history.rst @@ -13,6 +13,7 @@ Synopsis history merge history save history clear + history clear-session history ( -h | --help ) Description @@ -32,6 +33,8 @@ The following operations (sub-commands) are available: - ``clear`` clears the history file. A prompt is displayed before the history is erased asking you to confirm you really want to clear all history unless ``builtin history`` is used. +- ``clear-session`` clears the history file from all activity of the current session. Note: If ``history merge`` or ``builtin history merge`` is run in a session only the history after this will be erased. + The following options are available: These flags can appear before or immediately after one of the sub-commands listed above. diff --git a/share/completions/history.fish b/share/completions/history.fish index 5d80f2def..227544cd1 100644 --- a/share/completions/history.fish +++ b/share/completions/history.fish @@ -1,5 +1,5 @@ # Note that when a completion file is sourced a new block scope is created so `set -l` works. -set -l __fish_history_all_commands search delete save merge clear +set -l __fish_history_all_commands search delete save merge clear clear-session complete -c history -s h -l help -d "Display help and exit" @@ -33,3 +33,5 @@ complete -f -c history -n "not __fish_seen_subcommand_from $__fish_history_all_c -a merge -d "Incorporate history changes from other sessions" complete -f -c history -n "not __fish_seen_subcommand_from $__fish_history_all_commands" \ -a clear -d "Clears history file" +complete -f -c history -n "not __fish_seen_subcommand_from $__fish_history_all_commands" \ + -a clear-session -d "Clears all history from the current session" diff --git a/share/functions/history.fish b/share/functions/history.fish index b91885943..00e2c0c3f 100644 --- a/share/functions/history.fish +++ b/share/functions/history.fish @@ -62,13 +62,15 @@ function history --description "display or manipulate interactive command histor set hist_cmd search else if set -q _flag_merge set hist_cmd merge + else if set -q _flag_clear-session + set hist_cmd clear-session end # If a history command has not already been specified check the first non-flag argument for a # command. This allows the flags to appear before or after the subcommand. if not set -q hist_cmd[1] and set -q argv[1] - if contains $argv[1] search delete merge save clear + if contains $argv[1] search delete merge save clear clear-session set hist_cmd $argv[1] set -e argv[1] end @@ -190,7 +192,12 @@ function history --description "display or manipulate interactive command histor else printf (_ "You did not say 'yes' so I will not clear your command history\n") end + case clear-session # clears only session + __fish_unexpected_hist_args $argv + and return 1 + builtin history clear-session -- $argv + printf (_ "Command history for session cleared!\n") case '*' printf "%ls: unexpected subcommand '%ls'\n" $cmd $hist_cmd return 2 diff --git a/src/builtin_history.cpp b/src/builtin_history.cpp index 7f38dd5b0..de1a13f9f 100644 --- a/src/builtin_history.cpp +++ b/src/builtin_history.cpp @@ -21,12 +21,15 @@ #include "wgetopt.h" #include "wutil.h" // IWYU pragma: keep -enum hist_cmd_t { HIST_SEARCH = 1, HIST_DELETE, HIST_CLEAR, HIST_MERGE, HIST_SAVE, HIST_UNDEF }; +enum hist_cmd_t { HIST_SEARCH = 1, HIST_DELETE, HIST_CLEAR, HIST_MERGE, HIST_SAVE, HIST_UNDEF, + HIST_CLEAR_SESSION }; // Must be sorted by string, not enum or random. static const enum_map hist_enum_map[] = { - {HIST_CLEAR, L"clear"}, {HIST_DELETE, L"delete"}, {HIST_MERGE, L"merge"}, - {HIST_SAVE, L"save"}, {HIST_SEARCH, L"search"}, {HIST_UNDEF, nullptr}}; + {HIST_CLEAR, L"clear"}, {HIST_CLEAR_SESSION, L"clear-session"}, + {HIST_DELETE, L"delete"}, {HIST_MERGE, L"merge"}, + {HIST_SAVE, L"save"}, {HIST_SEARCH, L"search"}, + {HIST_UNDEF, nullptr}, }; struct history_cmd_opts_t { hist_cmd_t hist_cmd = HIST_UNDEF; @@ -287,6 +290,15 @@ maybe_t builtin_history(parser_t &parser, io_streams_t &streams, const wcha history->save(); break; } + case HIST_CLEAR_SESSION: { + if (check_for_unexpected_hist_args(opts, cmd, args, streams)) { + status = STATUS_INVALID_ARGS; + break; + } + history->clear_session(); + history->save(); + break; + } case HIST_MERGE: { if (check_for_unexpected_hist_args(opts, cmd, args, streams)) { status = STATUS_INVALID_ARGS; diff --git a/src/history.cpp b/src/history.cpp index b1cbad749..ea26e9c48 100644 --- a/src/history.cpp +++ b/src/history.cpp @@ -240,7 +240,9 @@ struct history_impl_t { uint32_t disable_automatic_save_counter{0}; // Deleted item contents. - std::unordered_set deleted_items{}; + // Boolean describes if it should be deleted only in this session or in all + // (used in deduplication). + std::unordered_map deleted_items{}; // The buffer containing the history file contents. std::unique_ptr file_contents{}; @@ -332,6 +334,9 @@ struct history_impl_t { // Irreversibly clears history. void clear(); + // Clears only session. + void clear_session(); + // Populates from older location ()in config path, rather than data path). void populate_from_config_path(); @@ -453,7 +458,7 @@ void history_impl_t::save_unless_disabled() { // case-sensitive, matches. void history_impl_t::remove(const wcstring &str_to_remove) { // Add to our list of deleted items. - deleted_items.insert(str_to_remove); + deleted_items.insert(std::pair(str_to_remove, false)); size_t idx = new_items.size(); while (idx--) { @@ -721,11 +726,22 @@ bool history_impl_t::rewrite_to_temporary_file(int existing_fd, int dst_fd) cons // Try decoding an old item. history_item_t old_item = local_file->decode_item(*offset); - if (old_item.empty() || deleted_items.count(old_item.str()) > 0) { - continue; + // If old item is newer than session always erase if in deleted. + if (old_item.timestamp() > boundary_timestamp) { + if (old_item.empty() || deleted_items.count(old_item.str()) > 0) { + continue; + } + lru.add_item(std::move(old_item)); + } else { + // If old item is older and in deleted items don't erase if added by + // clear_session. + if (old_item.empty() || (deleted_items.count(old_item.str()) > 0 && + !deleted_items.at(old_item.str()))) { + continue; + } + // Add this old item. + lru.add_item(std::move(old_item)); } - // Add this old item. - lru.add_item(std::move(old_item)); } } @@ -1073,6 +1089,15 @@ void history_impl_t::clear() { this->clear_file_state(); } +void history_impl_t::clear_session() { + for (const auto &item : new_items) { + deleted_items.insert(std::pair(item.str(), true)); + } + + new_items.clear(); + first_unwritten_new_item_index = 0; +} + bool history_impl_t::is_default() const { return name == DFLT_FISH_HISTORY_SESSION_ID; } bool history_impl_t::is_empty() { @@ -1491,6 +1516,8 @@ bool history_t::search(history_search_type_t search_type, const wcstring_list_t void history_t::clear() { impl()->clear(); } +void history_t::clear_session() { impl()->clear_session(); } + void history_t::populate_from_config_path() { impl()->populate_from_config_path(); } void history_t::populate_from_bash(FILE *f) { impl()->populate_from_bash(f); } diff --git a/src/history.h b/src/history.h index 6778d84fd..2877f273b 100644 --- a/src/history.h +++ b/src/history.h @@ -193,6 +193,9 @@ class history_t : noncopyable_t, nonmovable_t { // Irreversibly clears history. void clear(); + // Irreversibly clears history for the current session. + void clear_session(); + // Populates from older location (in config path, rather than data path). void populate_from_config_path(); diff --git a/tests/checks/history.fish b/tests/checks/history.fish index f05319bde..b7a1d7925 100644 --- a/tests/checks/history.fish +++ b/tests/checks/history.fish @@ -30,12 +30,16 @@ builtin history --clear abc def # First with the history function. history clear --contains #CHECKERR: history: you cannot use any options with the clear command +history clear-session --contains +#CHECKERR: history: you cannot use any options with the clear-session command history merge -t #CHECKERR: history: you cannot use any options with the merge command history save xyz #CHECKERR: history: save expected 0 args, got 1 history --prefix clear #CHECKERR: history: you cannot use any options with the clear command +history --prefix clear-session +#CHECKERR: history: you cannot use any options with the clear-session command history --show-time merge #CHECKERR: history: you cannot use any options with the merge command @@ -47,10 +51,14 @@ builtin history save --prefix #CHECKERR: history: you cannot use any options with the save command builtin history clear --show-time #CHECKERR: history: you cannot use any options with the clear command +builtin history clear-session --show-time +#CHECKERR: history: you cannot use any options with the clear-session command builtin history merge xyz #CHECKERR: history merge: Expected 0 args, got 1 builtin history clear abc def #CHECKERR: history clear: Expected 0 args, got 2 +builtin history clear-session abc def +#CHECKERR: history clear-session: Expected 0 args, got 2 builtin history --contains save #CHECKERR: history: you cannot use any options with the save command builtin history -t merge diff --git a/tests/pexpects/history.py b/tests/pexpects/history.py index 6da06e502..1d3368347 100644 --- a/tests/pexpects/history.py +++ b/tests/pexpects/history.py @@ -156,3 +156,25 @@ sendline(" ") expect_prompt() send("\x1b[A") expect_re("echo TERM") # not ephemeral! + +# Verify that clear-session works as expected +# Note: This test depends on that history merge resets the session from history clear-sessions point of view. +sendline("builtin history clear") +expect_prompt() + +# create before history +sendline("echo before1") +expect_prompt() +sendline("echo before2") +expect_prompt() +# "reset" session with history merge +sendline("history merge") +expect_prompt() +# create after history +sendline("echo after") +expect_prompt() +#clear session +sendline("history clear-session") +expect_prompt() +sendline("history search --exact 'echo after' | cat") +expect_prompt("\r\n")