mirror of
https://github.com/fish-shell/fish-shell
synced 2024-11-14 17:07:44 +00:00
783 lines
22 KiB
C++
783 lines
22 KiB
C++
/** \file history.c
|
|
History functions, part of the user interface.
|
|
*/
|
|
#include "config.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <wchar.h>
|
|
#include <errno.h>
|
|
#include <dirent.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include <sys/mman.h>
|
|
#include <fcntl.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <assert.h>
|
|
|
|
#include "fallback.h"
|
|
#include "util.h"
|
|
#include "sanity.h"
|
|
#include "tokenizer.h"
|
|
|
|
#include "wutil.h"
|
|
#include "history.h"
|
|
#include "common.h"
|
|
#include "intern.h"
|
|
#include "path.h"
|
|
#include "signal.h"
|
|
#include "autoload.h"
|
|
#include "iothread.h"
|
|
#include <map>
|
|
#include <algorithm>
|
|
|
|
/*
|
|
|
|
Our history format is intended to be valid YAML. Here it is:
|
|
|
|
- cmd: ssh blah blah blah
|
|
when: 2348237
|
|
paths:
|
|
- /path/to/something
|
|
- /path/to/something_else
|
|
|
|
Newlines are replaced by \n. Backslashes are replaced by \\.
|
|
*/
|
|
|
|
|
|
|
|
/** When we rewrite the history, the number of items we keep */
|
|
#define HISTORY_SAVE_MAX (1024 * 256)
|
|
|
|
/** Interval in seconds between automatic history save */
|
|
#define SAVE_INTERVAL (5*60)
|
|
|
|
/** Number of new history entries to add before automatic history save */
|
|
#define SAVE_COUNT 5
|
|
|
|
/* Our LRU cache is used for restricting the amount of history we have, and limiting how long we order it. */
|
|
class history_lru_node_t : public lru_node_t {
|
|
public:
|
|
time_t timestamp;
|
|
path_list_t required_paths;
|
|
history_lru_node_t(const history_item_t &item) :
|
|
lru_node_t(item.str()),
|
|
timestamp(item.timestamp()),
|
|
required_paths(item.required_paths)
|
|
{}
|
|
|
|
bool write_to_file(FILE *f) const;
|
|
bool write_yaml_to_file(FILE *f) const;
|
|
};
|
|
|
|
class history_lru_cache_t : public lru_cache_t<history_lru_node_t> {
|
|
protected:
|
|
|
|
/* Override to delete evicted nodes */
|
|
virtual void node_was_evicted(history_lru_node_t *node) {
|
|
delete node;
|
|
}
|
|
|
|
public:
|
|
history_lru_cache_t(size_t max) : lru_cache_t<history_lru_node_t>(max) { }
|
|
|
|
/* Function to add a history item */
|
|
void add_item(const history_item_t &item) {
|
|
/* Skip empty items */
|
|
if (item.empty())
|
|
return;
|
|
|
|
/* See if it's in the cache. If it is, update the timestamp. If not, we create a new node and add it. Note that calling get_node promotes the node to the front. */
|
|
history_lru_node_t *node = this->get_node(item.str());
|
|
if (node != NULL) {
|
|
node->timestamp = std::max(node->timestamp, item.timestamp());
|
|
/* What to do about paths here? Let's just ignore them */
|
|
} else {
|
|
node = new history_lru_node_t(item);
|
|
this->add_node(node);
|
|
}
|
|
}
|
|
};
|
|
|
|
static pthread_mutex_t hist_lock = PTHREAD_MUTEX_INITIALIZER;
|
|
|
|
static std::map<wcstring, history_t *> histories;
|
|
|
|
static wcstring history_filename(const wcstring &name, const wcstring &suffix);
|
|
|
|
/** Replaces newlines with a literal backslash followed by an n, and replaces backslashes with two backslashes. */
|
|
static void escape_yaml(std::string &str);
|
|
|
|
/** Undoes escape_yaml */
|
|
static void unescape_yaml(std::string &str);
|
|
|
|
/* Custom deleter for our shared_ptr */
|
|
class history_item_data_deleter_t {
|
|
private:
|
|
const bool free_it;
|
|
public:
|
|
history_item_data_deleter_t(bool flag) : free_it(flag) { }
|
|
void operator()(const wchar_t *data) {
|
|
if (free_it)
|
|
free((void *)data);
|
|
}
|
|
};
|
|
|
|
history_item_t::history_item_t(const wcstring &str) : contents(str), creation_timestamp(time(NULL))
|
|
{
|
|
}
|
|
|
|
history_item_t::history_item_t(const wcstring &str, time_t when, const path_list_t &paths) : contents(str), creation_timestamp(when), required_paths(paths)
|
|
{
|
|
}
|
|
|
|
bool history_item_t::matches_search(const wcstring &term, enum history_search_type_t type) const {
|
|
switch (type) {
|
|
|
|
case HISTORY_SEARCH_TYPE_CONTAINS:
|
|
/* We consider equal strings to NOT match a contains search (so that you don't have to see history equal to what you typed). The length check ensures that. */
|
|
return contents.size() > term.size() && contents.find(term) != wcstring::npos;
|
|
|
|
case HISTORY_SEARCH_TYPE_PREFIX:
|
|
/* We consider equal strings to match a prefix search, so that autosuggest will allow suggesting what you've typed */
|
|
return string_prefixes_string(term, contents);
|
|
|
|
default:
|
|
sanity_lose();
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/* Output our YAML to a file */
|
|
bool history_lru_node_t::write_yaml_to_file(FILE *f) const {
|
|
std::string cmd = wcs2string(key);
|
|
escape_yaml(cmd);
|
|
if (fprintf(f, "- cmd: %s\n", cmd.c_str()) < 0)
|
|
return false;
|
|
|
|
if (fprintf(f, " when: %ld\n", (long)timestamp) < 0)
|
|
return false;
|
|
|
|
if (! required_paths.empty()) {
|
|
if (fputs(" paths:\n", f) < 0)
|
|
return false;
|
|
|
|
for (path_list_t::const_iterator iter = required_paths.begin(); iter != required_paths.end(); iter++) {
|
|
std::string path = wcs2string(*iter);
|
|
escape_yaml(path);
|
|
|
|
if (fprintf(f, " - %s\n", path.c_str()) < 0)
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
history_t & history_t::history_with_name(const wcstring &name) {
|
|
/* Note that histories are currently never deleted, so we can return a reference to them without using something like shared_ptr */
|
|
scoped_lock locker(hist_lock);
|
|
history_t *& current = histories[name];
|
|
if (current == NULL)
|
|
current = new history_t(name);
|
|
return *current;
|
|
}
|
|
|
|
history_t::history_t(const wcstring &pname) :
|
|
name(pname),
|
|
mmap_start(NULL),
|
|
mmap_length(0),
|
|
save_timestamp(0),
|
|
loaded_old(false)
|
|
{
|
|
pthread_mutex_init(&lock, NULL);
|
|
}
|
|
|
|
history_t::~history_t()
|
|
{
|
|
pthread_mutex_destroy(&lock);
|
|
}
|
|
|
|
void history_t::add(const history_item_t &item)
|
|
{
|
|
scoped_lock locker(lock);
|
|
|
|
/* Add the history items */
|
|
new_items.push_back(item);
|
|
|
|
/* Prevent the first write from always triggering a save */
|
|
time_t now = time(NULL);
|
|
if (! save_timestamp)
|
|
save_timestamp = now;
|
|
|
|
/* This might be a good candidate for moving to a background thread */
|
|
if((now > save_timestamp + SAVE_INTERVAL) || (new_items.size() >= SAVE_COUNT))
|
|
this->save_internal();
|
|
|
|
}
|
|
|
|
void history_t::add(const wcstring &str, const path_list_t &valid_paths)
|
|
{
|
|
this->add(history_item_t(str, time(NULL), valid_paths));
|
|
}
|
|
|
|
history_item_t history_t::item_at_index(size_t idx) {
|
|
scoped_lock locker(lock);
|
|
|
|
/* 0 is considered an invalid index */
|
|
assert(idx > 0);
|
|
idx--;
|
|
|
|
/* idx=0 corresponds to last item in new_items */
|
|
size_t new_item_count = new_items.size();
|
|
if (idx < new_item_count) {
|
|
return new_items.at(new_item_count - idx - 1);
|
|
}
|
|
|
|
/* Now look in our old items */
|
|
idx -= new_item_count;
|
|
load_old_if_needed();
|
|
size_t old_item_count = old_item_offsets.size();
|
|
if (idx < old_item_count) {
|
|
/* idx=0 corresponds to last item in old_item_offsets */
|
|
size_t offset = old_item_offsets.at(old_item_count - idx - 1);
|
|
return history_t::decode_item(mmap_start + offset, mmap_length - offset);
|
|
}
|
|
|
|
/* Index past the valid range, so return an empty history item */
|
|
return history_item_t(wcstring(), 0);
|
|
}
|
|
|
|
/* Read one line, stripping off any newline, and updating cursor. Note that our string is NOT null terminated; it's just a memory mapped file. */
|
|
static size_t read_line(const char *base, size_t cursor, size_t len, std::string &result) {
|
|
/* Locate the newline */
|
|
assert(cursor <= len);
|
|
const char *start = base + cursor;
|
|
const char *newline = (char *)memchr(start, '\n', len - cursor);
|
|
if (newline != NULL) {
|
|
/* We found a newline. */
|
|
result.assign(start, newline - start);
|
|
|
|
/* Return the amount to advance the cursor; skip over the newline */
|
|
return newline - start + 1;
|
|
} else {
|
|
/* We ran off the end */
|
|
result.clear();
|
|
return len - cursor;
|
|
}
|
|
}
|
|
|
|
/* Trims leading spaces in the given string, returning how many there were */
|
|
static size_t trim_leading_spaces(std::string &str) {
|
|
size_t i = 0, max = str.size();
|
|
while (i < max && str[i] == ' ')
|
|
i++;
|
|
str.erase(0, i);
|
|
return i;
|
|
}
|
|
|
|
static bool extract_prefix(std::string &key, std::string &value, const std::string &line) {
|
|
size_t where = line.find(":");
|
|
if (where != std::string::npos) {
|
|
key = line.substr(0, where);
|
|
|
|
// skip a space after the : if necessary
|
|
size_t val_start = where + 1;
|
|
if (val_start < line.size() && line.at(val_start) == ' ')
|
|
val_start++;
|
|
value = line.substr(val_start);
|
|
|
|
unescape_yaml(key);
|
|
unescape_yaml(value);
|
|
}
|
|
return where != std::string::npos;
|
|
}
|
|
|
|
history_item_t history_t::decode_item(const char *base, size_t len) {
|
|
wcstring cmd;
|
|
time_t when = 0;
|
|
path_list_t paths;
|
|
|
|
size_t indent = 0, cursor = 0;
|
|
std::string key, value, line;
|
|
|
|
/* Read the "- cmd:" line */
|
|
size_t advance = read_line(base, cursor, len, line);
|
|
trim_leading_spaces(line);
|
|
if (! extract_prefix(key, value, line) || key != "- cmd")
|
|
goto done;
|
|
|
|
cursor += advance;
|
|
cmd = str2wcstring(value);
|
|
|
|
/* Read the remaining lines */
|
|
for (;;) {
|
|
/* Read a line */
|
|
size_t advance = read_line(base, cursor, len, line);
|
|
|
|
/* Count and trim leading spaces */
|
|
size_t this_indent = trim_leading_spaces(line);
|
|
if (indent == 0)
|
|
indent = this_indent;
|
|
|
|
if (this_indent == 0 || indent != this_indent)
|
|
break;
|
|
|
|
if (! extract_prefix(key, value, line))
|
|
break;
|
|
|
|
/* We are definitely going to consume this line */
|
|
unescape_yaml(value);
|
|
cursor += advance;
|
|
|
|
if (key == "when") {
|
|
/* Parse an int from the timestamp */
|
|
long tmp = 0;
|
|
if (sscanf(value.c_str(), "%ld", &tmp) > 0) {
|
|
when = tmp;
|
|
}
|
|
} else if (key == "paths") {
|
|
/* Read lines starting with " - " until we can't read any more */
|
|
for (;;) {
|
|
size_t advance = read_line(base, cursor, len, line);
|
|
if (trim_leading_spaces(line) <= indent)
|
|
break;
|
|
|
|
if (strncmp(line.c_str(), "- ", 2))
|
|
break;
|
|
|
|
/* We're going to consume this line */
|
|
cursor += advance;
|
|
|
|
|
|
/* Skip the leading dash-space and then store this path it */
|
|
line.erase(0, 2);
|
|
unescape_yaml(line);
|
|
paths.push_front(str2wcstring(line));
|
|
}
|
|
}
|
|
}
|
|
/* Reverse the paths, since we pushed them to the front each time */
|
|
done:
|
|
paths.reverse();
|
|
return history_item_t(cmd, when, paths);
|
|
|
|
}
|
|
|
|
void history_t::populate_from_mmap(void)
|
|
{
|
|
const char *begin = mmap_start;
|
|
size_t cursor = 0;
|
|
while (cursor < mmap_length) {
|
|
const char *line_start = begin + cursor;
|
|
/* Look for a newline */
|
|
const char *newline = (const char *)memchr(line_start, '\n', mmap_length - cursor);
|
|
if (newline == NULL)
|
|
break;
|
|
|
|
/* Advance the cursor past this line. +1 is for the newline */
|
|
size_t line_len = newline - line_start;
|
|
cursor += line_len + 1;
|
|
|
|
/* Skip lines with a leading, since these are in the interior of one of our items */
|
|
if (line_start[0] == ' ')
|
|
continue;
|
|
|
|
/* Skip very short lines to make one of the checks below easier */
|
|
if (line_len < 3)
|
|
continue;
|
|
|
|
/* Try to be a little YAML compatible. Skip lines with leading %, ---, or ... */
|
|
if (! memcmp(line_start, "%", 1) ||
|
|
! memcmp(line_start, "---", 3) ||
|
|
! memcmp(line_start, "...", 3))
|
|
continue;
|
|
|
|
/* We made it through the gauntlet. */
|
|
old_item_offsets.push_back(line_start - begin);
|
|
}
|
|
}
|
|
|
|
void history_t::load_old_if_needed(void)
|
|
{
|
|
if (loaded_old) return;
|
|
loaded_old = true;
|
|
|
|
int fd;
|
|
int ok=0;
|
|
|
|
// PCA not sure why signals were blocked here
|
|
//signal_block();
|
|
wcstring filename = history_filename(name, L"");
|
|
|
|
if( ! filename.empty() )
|
|
{
|
|
if( ( fd = wopen( filename, O_RDONLY ) ) > 0 )
|
|
{
|
|
off_t len = lseek( fd, 0, SEEK_END );
|
|
if( len != (off_t)-1)
|
|
{
|
|
mmap_length = (size_t)len;
|
|
if( lseek( fd, 0, SEEK_SET ) == 0 )
|
|
{
|
|
if( (mmap_start = (char *)mmap( 0, mmap_length, PROT_READ, MAP_PRIVATE, fd, 0 )) != MAP_FAILED )
|
|
{
|
|
ok = 1;
|
|
this->populate_from_mmap();
|
|
}
|
|
}
|
|
}
|
|
close( fd );
|
|
}
|
|
}
|
|
//signal_unblock();
|
|
}
|
|
|
|
void history_search_t::skip_matches(const wcstring_list_t &skips) {
|
|
external_skips = skips;
|
|
std::sort(external_skips.begin(), external_skips.end());
|
|
}
|
|
|
|
bool history_search_t::should_skip_match(const wcstring &str) const {
|
|
return std::binary_search(external_skips.begin(), external_skips.end(), str);
|
|
}
|
|
|
|
bool history_search_t::go_forwards() {
|
|
/* Pop the top index (if more than one) and return if we have any left */
|
|
if (prev_matches.size() > 1) {
|
|
prev_matches.pop_back();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool history_search_t::go_backwards() {
|
|
/* Backwards means increasing our index */
|
|
const size_t max_idx = (size_t)(-1);
|
|
|
|
size_t idx = 0;
|
|
if (! prev_matches.empty())
|
|
idx = prev_matches.back().first;
|
|
|
|
if (idx == max_idx)
|
|
return false;
|
|
|
|
while (++idx < max_idx) {
|
|
const history_item_t item = history->item_at_index(idx);
|
|
/* We're done if it's empty */
|
|
if (item.empty()) {
|
|
return false;
|
|
}
|
|
|
|
/* Look for a term that matches and that we haven't seen before */
|
|
const wcstring &str = item.str();
|
|
if (item.matches_search(term, search_type) && ! match_already_made(str) && ! should_skip_match(str)) {
|
|
prev_matches.push_back(prev_match_t(idx, item));
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/** Goes to the end (forwards) */
|
|
void history_search_t::go_to_end(void) {
|
|
prev_matches.clear();
|
|
}
|
|
|
|
/** Returns if we are at the end, which is where we start. */
|
|
bool history_search_t::is_at_end(void) const {
|
|
return prev_matches.empty();
|
|
}
|
|
|
|
|
|
/** Goes to the beginning (backwards) */
|
|
void history_search_t::go_to_beginning(void) {
|
|
/* Just go backwards as far as we can */
|
|
while (go_backwards())
|
|
;
|
|
}
|
|
|
|
|
|
history_item_t history_search_t::current_item() const {
|
|
assert(! prev_matches.empty());
|
|
return prev_matches.back().second;
|
|
}
|
|
|
|
wcstring history_search_t::current_string() const {
|
|
history_item_t item = this->current_item();
|
|
return item.str();
|
|
}
|
|
|
|
bool history_search_t::match_already_made(const wcstring &match) const {
|
|
for (std::deque<prev_match_t>::const_iterator iter = prev_matches.begin(); iter != prev_matches.end(); iter++) {
|
|
if (iter->second.str() == match)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
static void replace_all(std::string &str, const char *needle, const char *replacement)
|
|
{
|
|
size_t needle_len = strlen(needle), replacement_len = strlen(replacement);
|
|
size_t offset = 0;
|
|
while((offset = str.find(needle, offset)) != std::string::npos)
|
|
{
|
|
str.replace(offset, needle_len, replacement);
|
|
offset += replacement_len;
|
|
}
|
|
}
|
|
|
|
static void escape_yaml(std::string &str) {
|
|
replace_all(str, "\\", "\\\\"); //replace one backslash with two
|
|
replace_all(str, "\n", "\\n"); //replace newline with backslash + literal n
|
|
}
|
|
|
|
static void unescape_yaml(std::string &str) {
|
|
bool prev_escape = false;
|
|
for (size_t idx = 0; idx < str.size(); idx++) {
|
|
char c = str.at(idx);
|
|
if (prev_escape) {
|
|
if (c == '\\') {
|
|
/* Two backslashes in a row. Delete this one */
|
|
str.erase(idx, 1);
|
|
idx--;
|
|
} else if (c == 'n') {
|
|
/* Replace backslash + n with an actual newline */
|
|
str.replace(idx - 1, 2, "\n");
|
|
idx--;
|
|
}
|
|
prev_escape = false;
|
|
} else {
|
|
prev_escape = (c == '\\');
|
|
}
|
|
}
|
|
}
|
|
|
|
static wcstring history_filename(const wcstring &name, const wcstring &suffix)
|
|
{
|
|
wcstring path;
|
|
if (! path_get_config(path))
|
|
return L"";
|
|
|
|
wcstring result = path;
|
|
result.append(L"/");
|
|
result.append(name);
|
|
result.append(L"_history");
|
|
result.append(suffix);
|
|
return result;
|
|
}
|
|
|
|
void history_t::clear_file_state()
|
|
{
|
|
/* Erase everything we know about our file */
|
|
if (mmap_start != NULL && mmap_start != MAP_FAILED) {
|
|
munmap((void *)mmap_start, mmap_length);
|
|
}
|
|
mmap_start = 0;
|
|
mmap_length = 0;
|
|
loaded_old = false;
|
|
new_items.clear();
|
|
old_item_offsets.clear();
|
|
save_timestamp=time(0);
|
|
}
|
|
|
|
/** Save the specified mode to file */
|
|
void history_t::save_internal()
|
|
{
|
|
/* This must be called while locked */
|
|
|
|
/* Nothing to do if there's no new items */
|
|
if (new_items.empty())
|
|
return;
|
|
|
|
bool ok = true;
|
|
|
|
wcstring tmp_name = history_filename(name, L".tmp");
|
|
if( ! tmp_name.empty() )
|
|
{
|
|
|
|
/* Load old */
|
|
load_old_if_needed();
|
|
|
|
/* Make an LRU cache to save only the last N elements */
|
|
history_lru_cache_t lru(HISTORY_SAVE_MAX);
|
|
|
|
/* Insert old items in, from old to new */
|
|
for (std::deque<size_t>::iterator iter = old_item_offsets.begin(); iter != old_item_offsets.end(); iter++) {
|
|
size_t offset = *iter;
|
|
history_item_t item = history_t::decode_item(mmap_start + offset, mmap_length - offset);
|
|
lru.add_item(item);
|
|
}
|
|
|
|
/* Insert new items */
|
|
for (std::vector<history_item_t>::iterator iter = new_items.begin(); iter != new_items.end(); iter++) {
|
|
lru.add_item(*iter);
|
|
}
|
|
|
|
signal_block();
|
|
|
|
FILE *out;
|
|
if( (out=wfopen( tmp_name, "w" ) ) )
|
|
{
|
|
/* Write them out */
|
|
for (history_lru_cache_t::iterator iter = lru.begin(); iter != lru.end(); iter++) {
|
|
const history_lru_node_t *node = *iter;
|
|
if (! node->write_yaml_to_file(out)) {
|
|
ok = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if( fclose( out ) || !ok )
|
|
{
|
|
/*
|
|
This message does not have high enough priority to
|
|
be shown by default.
|
|
*/
|
|
debug( 2, L"Error when writing history file" );
|
|
}
|
|
else
|
|
{
|
|
wcstring new_name = history_filename(name, wcstring());
|
|
wrename(tmp_name, new_name);
|
|
}
|
|
}
|
|
|
|
signal_unblock();
|
|
|
|
/* Make sure we clear all nodes, since this doesn't happen automatically */
|
|
lru.evict_all_nodes();
|
|
}
|
|
|
|
if( ok )
|
|
{
|
|
/* Our history has been written to the file, so clear our state so we can re-reference the file. */
|
|
this->clear_file_state();
|
|
}
|
|
}
|
|
|
|
void history_t::save(void) {
|
|
scoped_lock locker(lock);
|
|
this->save_internal();
|
|
}
|
|
|
|
void history_t::clear(void) {
|
|
scoped_lock locker(lock);
|
|
new_items.clear();
|
|
old_item_offsets.clear();
|
|
wcstring filename = history_filename(name, L"");
|
|
if (! filename.empty())
|
|
wunlink(filename);
|
|
this->clear_file_state();
|
|
|
|
}
|
|
|
|
void history_init()
|
|
{
|
|
}
|
|
|
|
|
|
void history_destroy()
|
|
{
|
|
/* Save all histories */
|
|
for (std::map<wcstring, history_t *>::iterator iter = histories.begin(); iter != histories.end(); iter++) {
|
|
iter->second->save();
|
|
}
|
|
}
|
|
|
|
|
|
void history_sanity_check()
|
|
{
|
|
/*
|
|
No sanity checking implemented yet...
|
|
*/
|
|
}
|
|
|
|
int file_detection_context_t::perform_file_detection(bool test_all) {
|
|
ASSERT_IS_BACKGROUND_THREAD();
|
|
valid_paths.clear();
|
|
int result = 1;
|
|
for (path_list_t::const_iterator iter = potential_paths.begin(); iter != potential_paths.end(); iter++) {
|
|
if (path_is_valid(*iter, working_directory)) {
|
|
/* Push the original (possibly relative) path */
|
|
valid_paths.push_front(*iter);
|
|
} else {
|
|
/* Not a valid path */
|
|
result = 0;
|
|
if (! test_all)
|
|
break;
|
|
}
|
|
}
|
|
valid_paths.reverse();
|
|
return result;
|
|
}
|
|
|
|
bool file_detection_context_t::paths_are_valid(const path_list_t &paths) {
|
|
this->potential_paths = paths;
|
|
return perform_file_detection(false) > 0;
|
|
}
|
|
|
|
file_detection_context_t::file_detection_context_t(history_t *hist, const wcstring &cmd) :
|
|
history(hist),
|
|
command(cmd),
|
|
when(time(NULL)),
|
|
working_directory(get_working_directory())
|
|
{
|
|
}
|
|
|
|
static int threaded_perform_file_detection(file_detection_context_t *ctx) {
|
|
ASSERT_IS_BACKGROUND_THREAD();
|
|
assert(ctx != NULL);
|
|
return ctx->perform_file_detection(true /* test all */);
|
|
}
|
|
|
|
static void perform_file_detection_done(file_detection_context_t *ctx, int success) {
|
|
/* Now that file detection is done, create the history item */
|
|
ctx->history->add(ctx->command, ctx->valid_paths);
|
|
|
|
/* Done with the context. */
|
|
delete ctx;
|
|
}
|
|
|
|
static bool string_could_be_path(const wcstring &potential_path) {
|
|
// Assume that things with leading dashes aren't paths
|
|
if (potential_path.empty() || potential_path.at(0) == L'-')
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
void history_t::add_with_file_detection(const wcstring &str)
|
|
{
|
|
ASSERT_IS_MAIN_THREAD();
|
|
path_list_t potential_paths;
|
|
|
|
tokenizer tokenizer;
|
|
for( tok_init( &tokenizer, str.c_str(), TOK_SQUASH_ERRORS );
|
|
tok_has_next( &tokenizer );
|
|
tok_next( &tokenizer ) )
|
|
{
|
|
int type = tok_last_type( &tokenizer );
|
|
if (type == TOK_STRING) {
|
|
const wchar_t *token_cstr = tok_last(&tokenizer);
|
|
if (token_cstr) {
|
|
wcstring potential_path = token_cstr;
|
|
if (unescape_string(potential_path, false) && string_could_be_path(potential_path)) {
|
|
potential_paths.push_front(potential_path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
tok_destroy(&tokenizer);
|
|
|
|
if (! potential_paths.empty()) {
|
|
/* We have some paths. Make a context. */
|
|
file_detection_context_t *context = new file_detection_context_t(this, str);
|
|
|
|
/* Store the potential paths. Reverse them to put them in the same order as in the command. */
|
|
potential_paths.reverse();
|
|
context->potential_paths.swap(potential_paths);
|
|
iothread_perform(threaded_perform_file_detection, perform_file_detection_done, context);
|
|
}
|
|
}
|
|
|