Changes to make autosuggestion smarter about not suggesting commands that could never succeed.
This commit is contained in:
parent
a92d9d442b
commit
a08450bcb6
@ -54,7 +54,8 @@
|
||||
/**
|
||||
The number of tests to run
|
||||
*/
|
||||
#define ESCAPE_TEST_COUNT 1000000
|
||||
//#define ESCAPE_TEST_COUNT 1000000
|
||||
#define ESCAPE_TEST_COUNT 10000
|
||||
/**
|
||||
The average length of strings to unescape
|
||||
*/
|
||||
@ -764,6 +765,7 @@ static void test_history_matches(history_search_t &search, size_t matches) {
|
||||
size_t i;
|
||||
for (i=0; i < matches; i++) {
|
||||
assert(search.go_backwards());
|
||||
wcstring item = search.current_string();
|
||||
}
|
||||
assert(! search.go_backwards());
|
||||
|
||||
@ -773,29 +775,79 @@ static void test_history_matches(history_search_t &search, size_t matches) {
|
||||
assert(! search.go_forwards());
|
||||
}
|
||||
|
||||
static void test_history(void) {
|
||||
class history_tests_t {
|
||||
public:
|
||||
static void test_history(void);
|
||||
};
|
||||
|
||||
static wcstring random_string(void) {
|
||||
wcstring result;
|
||||
size_t max = 1 + rand() % 32;
|
||||
while (max--) {
|
||||
wchar_t c = 1 + rand()%ESCAPE_TEST_CHAR;
|
||||
result.push_back(c);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void history_tests_t::test_history(void) {
|
||||
say( L"Testing history");
|
||||
|
||||
history_t &history = history_t::history_with_name(L"test_history");
|
||||
history.clear();
|
||||
history.add(L"Gamma");
|
||||
history.add(L"Beta");
|
||||
history.add(L"Alpha");
|
||||
|
||||
|
||||
/* All three items match "a" */
|
||||
history_search_t search1(history, L"a");
|
||||
test_history_matches(search1, 3);
|
||||
assert(search1.current_item() == L"Alpha");
|
||||
assert(search1.current_string() == L"Alpha");
|
||||
|
||||
/* One item matches "et" */
|
||||
history_search_t search2(history, L"et");
|
||||
test_history_matches(search2, 1);
|
||||
assert(search2.current_item() == L"Beta");
|
||||
assert(search2.current_string() == L"Beta");
|
||||
|
||||
/* Test history escaping and unescaping, yaml, etc. */
|
||||
std::vector<history_item_t> before, after;
|
||||
history.clear();
|
||||
size_t i, max = 100;
|
||||
for (i=1; i <= max; i++) {
|
||||
|
||||
/* Generate a value */
|
||||
wcstring value = wcstring(L"test item ") + format_val(i);
|
||||
|
||||
/* Generate some paths */
|
||||
path_list_t paths;
|
||||
size_t count = rand() % 6;
|
||||
while (count--) {
|
||||
paths.push_front(random_string());
|
||||
}
|
||||
|
||||
/* Record this item */
|
||||
history_item_t item(value, time(NULL), paths);
|
||||
before.push_back(item);
|
||||
history.add(item);
|
||||
}
|
||||
history.save();
|
||||
|
||||
/* Read items back in reverse order and ensure they're the same */
|
||||
for (i=100; i >= 1; i--) {
|
||||
history_item_t item = history.item_at_index(i);
|
||||
assert(! item.empty());
|
||||
after.push_back(item);
|
||||
}
|
||||
assert(before.size() == after.size());
|
||||
for (size_t i=0; i < before.size(); i++) {
|
||||
const history_item_t &bef = before.at(i), &aft = after.at(i);
|
||||
assert(bef.contents == aft.contents);
|
||||
assert(bef.creation_timestamp == aft.creation_timestamp);
|
||||
assert(bef.required_paths == aft.required_paths);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
Main test
|
||||
*/
|
||||
@ -825,7 +877,7 @@ int main( int argc, char **argv )
|
||||
test_expand();
|
||||
test_path();
|
||||
test_colors();
|
||||
test_history();
|
||||
history_tests_t::test_history();
|
||||
|
||||
say( L"Encountered %d errors in low-level tests", err_count );
|
||||
|
||||
|
512
history.cpp
512
history.cpp
@ -33,6 +33,21 @@
|
||||
#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)
|
||||
|
||||
@ -46,9 +61,15 @@
|
||||
class history_lru_node_t : public lru_node_t {
|
||||
public:
|
||||
time_t timestamp;
|
||||
history_lru_node_t(const history_item_t &item) : lru_node_t(item.str()), timestamp(item.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> {
|
||||
@ -72,6 +93,7 @@ class history_lru_cache_t : public lru_cache_t<history_lru_node_t> {
|
||||
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);
|
||||
@ -85,11 +107,11 @@ static std::map<wcstring, history_t *> histories;
|
||||
|
||||
static wcstring history_filename(const wcstring &name, const wcstring &suffix);
|
||||
|
||||
/* Unescapes newlines in-place */
|
||||
static void unescape_newlines(wcstring &str);
|
||||
/** Replaces newlines with a literal backslash followed by an n, and replaces backslashes with two backslashes. */
|
||||
static void escape_yaml(std::string &str);
|
||||
|
||||
/* Escapes newlines in-place */
|
||||
static void escape_newlines(wcstring &str);
|
||||
/** Undoes escape_yaml */
|
||||
static void unescape_yaml(std::string &str);
|
||||
|
||||
/* Custom deleter for our shared_ptr */
|
||||
class history_item_data_deleter_t {
|
||||
@ -128,10 +150,29 @@ bool history_item_t::matches_search(const wcstring &term, enum history_search_ty
|
||||
}
|
||||
}
|
||||
|
||||
bool history_lru_node_t::write_to_file(FILE *f) const {
|
||||
wcstring escaped = key;
|
||||
escape_newlines(escaped);
|
||||
return fwprintf( f, L"# %d\n%ls\n", timestamp, escaped.c_str());
|
||||
/* 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) {
|
||||
@ -158,17 +199,27 @@ history_t::~history_t()
|
||||
pthread_mutex_destroy(&lock);
|
||||
}
|
||||
|
||||
void history_t::add(const wcstring &str, const path_list_t &valid_paths)
|
||||
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);
|
||||
new_items.push_back(history_item_t(str, now, valid_paths));
|
||||
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) {
|
||||
@ -198,136 +249,151 @@ history_item_t history_t::item_at_index(size_t idx) {
|
||||
return history_item_t(wcstring(), 0);
|
||||
}
|
||||
|
||||
history_item_t history_t::decode_item(const char *begin, size_t len)
|
||||
{
|
||||
const char *pos = begin, *end = begin + len;
|
||||
|
||||
int was_backslash = 0;
|
||||
|
||||
wcstring output;
|
||||
time_t timestamp = 0;
|
||||
|
||||
int first_char = 1;
|
||||
bool timestamp_mode = false;
|
||||
|
||||
while( 1 )
|
||||
{
|
||||
wchar_t c;
|
||||
mbstate_t state;
|
||||
bzero(&state, sizeof state);
|
||||
|
||||
size_t res;
|
||||
|
||||
res = mbrtowc( &c, pos, end-pos, &state );
|
||||
|
||||
if( res == (size_t)-1 )
|
||||
{
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
else if( res == (size_t)-2 )
|
||||
{
|
||||
break;
|
||||
}
|
||||
else if( res == (size_t)0 )
|
||||
{
|
||||
pos++;
|
||||
continue;
|
||||
}
|
||||
pos += res;
|
||||
|
||||
if( c == L'\n' )
|
||||
{
|
||||
if( timestamp_mode )
|
||||
{
|
||||
const wchar_t *time_string = output.c_str();
|
||||
while( *time_string && !iswdigit(*time_string))
|
||||
time_string++;
|
||||
errno=0;
|
||||
|
||||
if( *time_string )
|
||||
{
|
||||
time_t tm;
|
||||
wchar_t *end;
|
||||
|
||||
errno = 0;
|
||||
tm = (time_t)wcstol( time_string, &end, 10 );
|
||||
|
||||
if( tm && !errno && !*end )
|
||||
{
|
||||
timestamp = tm;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
output.clear();
|
||||
timestamp_mode = 0;
|
||||
continue;
|
||||
}
|
||||
if( !was_backslash )
|
||||
break;
|
||||
}
|
||||
|
||||
if( first_char )
|
||||
{
|
||||
if( c == L'#' )
|
||||
timestamp_mode = 1;
|
||||
}
|
||||
|
||||
first_char = 0;
|
||||
|
||||
output.push_back(c);
|
||||
|
||||
was_backslash = ( (c == L'\\') && !was_backslash);
|
||||
/* 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);
|
||||
}
|
||||
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.c_str());
|
||||
|
||||
/* 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.c_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Reverse the paths, since we pushed them to the front each time */
|
||||
done:
|
||||
paths.reverse();
|
||||
return history_item_t(cmd, when, paths);
|
||||
|
||||
unescape_newlines(output);
|
||||
return history_item_t(output, timestamp);
|
||||
}
|
||||
|
||||
void history_t::populate_from_mmap(void)
|
||||
{
|
||||
const char *begin = mmap_start;
|
||||
const char *end = begin + mmap_length;
|
||||
const char *pos;
|
||||
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;
|
||||
|
||||
int ignore_newline = 0;
|
||||
int do_push = 1;
|
||||
/* Advance the cursor past this line. +1 is for the newline */
|
||||
size_t line_len = newline - line_start;
|
||||
cursor += line_len + 1;
|
||||
|
||||
for( pos = begin; pos <end; pos++ )
|
||||
{
|
||||
/* Skip lines with a leading, since these are in the interior of one of our items */
|
||||
if (line_start[0] == ' ')
|
||||
continue;
|
||||
|
||||
if( do_push )
|
||||
{
|
||||
ignore_newline = *pos == '#';
|
||||
/* Need to unique-ize */
|
||||
old_item_offsets.push_back(pos - begin);
|
||||
do_push = 0;
|
||||
}
|
||||
/* Skip very short lines to make one of the checks below easier */
|
||||
if (line_len < 3)
|
||||
continue;
|
||||
|
||||
switch( *pos )
|
||||
{
|
||||
case '\\':
|
||||
{
|
||||
pos++;
|
||||
break;
|
||||
}
|
||||
/* 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;
|
||||
|
||||
case '\n':
|
||||
{
|
||||
if( ignore_newline )
|
||||
{
|
||||
ignore_newline = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
do_push = 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* We made it through the gauntlet. */
|
||||
old_item_offsets.push_back(line_start - begin);
|
||||
}
|
||||
}
|
||||
|
||||
void history_t::load_old_if_needed(void)
|
||||
@ -403,7 +469,7 @@ bool history_search_t::go_backwards() {
|
||||
/* 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.str()));
|
||||
prev_matches.push_back(prev_match_t(idx, item));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -429,45 +495,59 @@ void history_search_t::go_to_beginning(void) {
|
||||
}
|
||||
|
||||
|
||||
wcstring history_search_t::current_item() const {
|
||||
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 == match)
|
||||
if (iter->second.str() == match)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
static void replace_all(wcstring &str, const wchar_t *needle, const wchar_t *replacement)
|
||||
static void replace_all(std::string &str, const char *needle, const char *replacement)
|
||||
{
|
||||
size_t needle_len = wcslen(needle);
|
||||
size_t needle_len = strlen(needle), replacement_len = strlen(replacement);
|
||||
size_t offset = 0;
|
||||
while((offset = str.find(needle, offset)) != wcstring::npos)
|
||||
while((offset = str.find(needle, offset)) != std::string::npos)
|
||||
{
|
||||
str.replace(offset, needle_len, replacement);
|
||||
offset += needle_len;
|
||||
offset += replacement_len;
|
||||
}
|
||||
}
|
||||
|
||||
static void unescape_newlines(wcstring &str)
|
||||
{
|
||||
/* Replace instances of backslash + newline with just the newline */
|
||||
replace_all(str, L"\\\n", L"\n");
|
||||
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 escape_newlines(wcstring &str)
|
||||
{
|
||||
/* Replace instances of newline with backslash + newline with newline */
|
||||
replace_all(str, L"\\\n", L"\n");
|
||||
|
||||
/* If the string ends with a backslash, we'll combine it with the next line. Hack around that by appending a newline. */
|
||||
if (! str.empty() && str.at(str.size() - 1) == L'\\')
|
||||
str.push_back('\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)
|
||||
@ -484,6 +564,20 @@ static wcstring history_filename(const wcstring &name, const wcstring &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()
|
||||
{
|
||||
@ -511,7 +605,7 @@ void history_t::save_internal()
|
||||
history_lru_cache_t lru(HISTORY_SAVE_MAX);
|
||||
|
||||
/* Insert old items in, from old to new */
|
||||
for (std::vector<size_t>::iterator iter = old_item_offsets.begin(); iter != old_item_offsets.end(); iter++) {
|
||||
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);
|
||||
@ -524,7 +618,8 @@ void history_t::save_internal()
|
||||
|
||||
/* Write them out */
|
||||
for (history_lru_cache_t::iterator iter = lru.begin(); iter != lru.end(); iter++) {
|
||||
if (! (*iter)->write_to_file(out)) {
|
||||
const history_lru_node_t *node = *iter;
|
||||
if (! node->write_yaml_to_file(out)) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
@ -550,15 +645,7 @@ void history_t::save_internal()
|
||||
if( ok )
|
||||
{
|
||||
/* Our history has been written to the file, so clear our state so we can re-reference the 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);
|
||||
this->clear_file_state();
|
||||
}
|
||||
|
||||
signal_unblock();
|
||||
@ -569,6 +656,17 @@ void history_t::save(void) {
|
||||
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.c_str());
|
||||
this->clear_file_state();
|
||||
|
||||
}
|
||||
|
||||
void history_init()
|
||||
{
|
||||
}
|
||||
@ -590,46 +688,69 @@ void history_sanity_check()
|
||||
*/
|
||||
}
|
||||
|
||||
struct file_detection_context_t {
|
||||
/* The history associated with this context */
|
||||
history_t *history;
|
||||
|
||||
/* The command */
|
||||
wcstring command;
|
||||
|
||||
/* The working directory at the time the command was issued */
|
||||
wcstring working_directory;
|
||||
|
||||
/* Paths to test */
|
||||
path_list_t potential_paths;
|
||||
|
||||
/* Paths that were found to be valid */
|
||||
path_list_t valid_paths;
|
||||
|
||||
int perform_file_detection() {
|
||||
ASSERT_IS_BACKGROUND_THREAD();
|
||||
for (path_list_t::const_iterator iter = potential_paths.begin(); iter != potential_paths.end(); iter++) {
|
||||
wcstring path = *iter;
|
||||
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++) {
|
||||
wcstring path = *iter;
|
||||
|
||||
bool path_is_valid;
|
||||
/* Some special paths are always valid */
|
||||
if (path.empty()) {
|
||||
path_is_valid = false;
|
||||
} else if (path == L"." || path == L"./") {
|
||||
path_is_valid = true;
|
||||
} else if (path == L".." || path == L"../") {
|
||||
path_is_valid = (! working_directory.empty() && working_directory != L"/");
|
||||
} else {
|
||||
/* Maybe append the working directory. Note that we know path is not empty here. */
|
||||
if (path.at(0) != '/') {
|
||||
path.insert(0, working_directory);
|
||||
}
|
||||
|
||||
if (0 == waccess(path.c_str(), F_OK)) {
|
||||
/* Push the original (possibly relative) path */
|
||||
valid_paths.push_front(*iter);
|
||||
}
|
||||
path_is_valid = (0 == waccess(path.c_str(), F_OK));
|
||||
}
|
||||
|
||||
|
||||
if (path_is_valid) {
|
||||
/* 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 0;
|
||||
}
|
||||
};
|
||||
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)) {
|
||||
/* Stash the working directory. TODO: We should be respecting CDPATH here*/
|
||||
wchar_t dir_path[4096];
|
||||
const wchar_t *cwd = wgetcwd( dir_path, 4096 );
|
||||
if (cwd) {
|
||||
wcstring wd = cwd;
|
||||
/* Make sure the working directory ends with a slash */
|
||||
if (! wd.empty() && wd.at(wd.size() - 1) != L'/')
|
||||
wd.push_back(L'/');
|
||||
working_directory.swap(wd);
|
||||
}
|
||||
}
|
||||
|
||||
static int threaded_perform_file_detection(file_detection_context_t *ctx) {
|
||||
ASSERT_IS_BACKGROUND_THREAD();
|
||||
assert(ctx != NULL);
|
||||
return ctx->perform_file_detection();
|
||||
return ctx->perform_file_detection(true /* test all */);
|
||||
}
|
||||
|
||||
static void perform_file_detection_done(file_detection_context_t *ctx, int success) {
|
||||
@ -671,20 +792,7 @@ void history_t::add_with_file_detection(const wcstring &str)
|
||||
|
||||
if (! potential_paths.empty()) {
|
||||
/* We have some paths. Make a context. */
|
||||
file_detection_context_t *context = new file_detection_context_t();
|
||||
context->command = str;
|
||||
context->history = this;
|
||||
|
||||
/* Stash the working directory. */
|
||||
wchar_t dir_path[4096];
|
||||
const wchar_t *cwd = wgetcwd( dir_path, 4096 );
|
||||
if (cwd) {
|
||||
wcstring wd = cwd;
|
||||
/* Make sure the working directory ends with a slash */
|
||||
if (! wd.empty() && wd.at(wd.size() - 1) != L'/')
|
||||
wd.push_back(L'/');
|
||||
context->working_directory.swap(wd);
|
||||
}
|
||||
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();
|
||||
|
78
history.h
78
history.h
@ -28,10 +28,12 @@ enum history_search_type_t {
|
||||
|
||||
class history_item_t {
|
||||
friend class history_t;
|
||||
friend class history_lru_node_t;
|
||||
friend class history_tests_t;
|
||||
|
||||
private:
|
||||
history_item_t(const wcstring &);
|
||||
history_item_t(const wcstring &, time_t, const path_list_t &paths = path_list_t());
|
||||
explicit history_item_t(const wcstring &);
|
||||
explicit history_item_t(const wcstring &, time_t, const path_list_t &paths = path_list_t());
|
||||
|
||||
/** The actual contents of the entry */
|
||||
wcstring contents;
|
||||
@ -51,10 +53,22 @@ class history_item_t {
|
||||
|
||||
time_t timestamp() const { return creation_timestamp; }
|
||||
|
||||
const path_list_t &get_required_paths() const { return required_paths; }
|
||||
|
||||
bool write_to_file(FILE *f) const;
|
||||
|
||||
bool operator==(const history_item_t &other) const {
|
||||
return contents == other.contents &&
|
||||
creation_timestamp == other.creation_timestamp &&
|
||||
required_paths == other.required_paths;
|
||||
}
|
||||
|
||||
/* Functions for testing only */
|
||||
|
||||
};
|
||||
|
||||
class history_t {
|
||||
friend class history_tests_t;
|
||||
private:
|
||||
/** No copying */
|
||||
history_t(const history_t&);
|
||||
@ -63,12 +77,18 @@ private:
|
||||
/** Private creator */
|
||||
history_t(const wcstring &pname);
|
||||
|
||||
/** Privately add an item */
|
||||
void add(const history_item_t &item);
|
||||
|
||||
/** Destructor */
|
||||
~history_t();
|
||||
|
||||
/** Lock for thread safety */
|
||||
pthread_mutex_t lock;
|
||||
|
||||
/** Internal function */
|
||||
void clear_file_state();
|
||||
|
||||
/** The name of this list. Used for picking a suitable filename and for switching modes. */
|
||||
const wcstring name;
|
||||
|
||||
@ -91,7 +111,7 @@ private:
|
||||
void populate_from_mmap(void);
|
||||
|
||||
/** List of old items, as offsets into out mmap data */
|
||||
std::vector<size_t> old_item_offsets;
|
||||
std::deque<size_t> old_item_offsets;
|
||||
|
||||
/** Whether we've loaded old items */
|
||||
bool loaded_old;
|
||||
@ -115,7 +135,10 @@ public:
|
||||
/** Saves history */
|
||||
void save();
|
||||
|
||||
/** Return the specified history at the specified index. 0 is the index of the current commandline. */
|
||||
/** Irreversibly clears history */
|
||||
void clear();
|
||||
|
||||
/** Return the specified history at the specified index. 0 is the index of the current commandline. (So the most recent item is at index 1.) */
|
||||
history_item_t item_at_index(size_t idx);
|
||||
};
|
||||
|
||||
@ -128,7 +151,7 @@ class history_search_t {
|
||||
enum history_search_type_t search_type;
|
||||
|
||||
/** Our list of previous matches as index, value. The end is the current match. */
|
||||
typedef std::pair<size_t, wcstring> prev_match_t;
|
||||
typedef std::pair<size_t, history_item_t> prev_match_t;
|
||||
std::deque<prev_match_t> prev_matches;
|
||||
|
||||
/** Returns yes if a given term is in prev_matches. */
|
||||
@ -144,6 +167,9 @@ class history_search_t {
|
||||
|
||||
public:
|
||||
|
||||
/** Gets the search term */
|
||||
const wcstring &get_term() const { return term; }
|
||||
|
||||
/** Sets additional string matches to skip */
|
||||
void skip_matches(const wcstring_list_t &skips);
|
||||
|
||||
@ -162,8 +188,12 @@ class history_search_t {
|
||||
/** Goes to the beginning (backwards) */
|
||||
void go_to_beginning(void);
|
||||
|
||||
/** Returns the current search result. asserts if there is no current item. */
|
||||
wcstring current_item(void) const;
|
||||
/** Returns the current search result item. asserts if there is no current item. */
|
||||
history_item_t current_item(void) const;
|
||||
|
||||
/** Returns the current search result item contents. asserts if there is no current item. */
|
||||
wcstring current_string(void) const;
|
||||
|
||||
|
||||
/** Constructor */
|
||||
history_search_t(history_t &hist, const wcstring &str, enum history_search_type_t type = HISTORY_SEARCH_TYPE_CONTAINS) :
|
||||
@ -199,4 +229,38 @@ void history_destroy();
|
||||
*/
|
||||
void history_sanity_check();
|
||||
|
||||
/* A helper class for threaded detection of paths */
|
||||
struct file_detection_context_t {
|
||||
|
||||
/* Constructor */
|
||||
file_detection_context_t(history_t *hist, const wcstring &cmd);
|
||||
|
||||
/* Determine which of potential_paths are valid, and put them in valid_paths */
|
||||
int perform_file_detection();
|
||||
|
||||
/* The history associated with this context */
|
||||
history_t *history;
|
||||
|
||||
/* The command */
|
||||
wcstring command;
|
||||
|
||||
/* When the command was issued */
|
||||
time_t when;
|
||||
|
||||
/* The working directory at the time the command was issued */
|
||||
wcstring working_directory;
|
||||
|
||||
/* Paths to test */
|
||||
path_list_t potential_paths;
|
||||
|
||||
/* Paths that were found to be valid */
|
||||
path_list_t valid_paths;
|
||||
|
||||
/* Performs file detection. Returns 1 if every path in potential_paths is valid, 0 otherwise. If test_all is true, tests every path; otherwise stops as soon as it reaches an invalid path. */
|
||||
int perform_file_detection(bool test_all);
|
||||
|
||||
/* Determine whether the given paths are all valid */
|
||||
bool paths_are_valid(const path_list_t &paths);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@ -716,7 +716,7 @@ void parser_t::destroy()
|
||||
lineinfo = 0;
|
||||
}
|
||||
|
||||
forbidden_function.resize(0);
|
||||
forbidden_function.clear();
|
||||
|
||||
}
|
||||
|
||||
@ -2409,7 +2409,7 @@ int parser_t::eval( const wcstring &cmdStr, io_data_t *io, enum block_type_t blo
|
||||
|
||||
if( block_type == SUBST )
|
||||
{
|
||||
forbidden_function.resize(0);
|
||||
forbidden_function.clear();
|
||||
}
|
||||
|
||||
CHECK_BLOCK( 1 );
|
||||
|
7
path.cpp
7
path.cpp
@ -497,10 +497,13 @@ void path_make_canonical( wcstring &path )
|
||||
{
|
||||
|
||||
/* Remove double slashes */
|
||||
replace_all(path, L"//", L"/");
|
||||
size_t size;
|
||||
do {
|
||||
size = path.size();
|
||||
replace_all(path, L"//", L"/");
|
||||
} while (path.size() != size);
|
||||
|
||||
/* Remove trailing slashes */
|
||||
size_t size = path.size();
|
||||
while (size--) {
|
||||
if (path.at(size) != L'/')
|
||||
break;
|
||||
|
3
proc.cpp
3
proc.cpp
@ -566,8 +566,9 @@ void proc_fire_event( const wchar_t *msg, int type, pid_t pid, int status )
|
||||
event.arguments->resize(0);
|
||||
}
|
||||
|
||||
int job_reap( int interactive )
|
||||
int job_reap( bool interactive )
|
||||
{
|
||||
ASSERT_IS_MAIN_THREAD();
|
||||
job_t *jnext;
|
||||
int found=0;
|
||||
|
||||
|
6
proc.h
6
proc.h
@ -475,9 +475,7 @@ void job_free( job_t* j );
|
||||
void job_promote(job_t *job);
|
||||
|
||||
/**
|
||||
Create a new job. Job struct is allocated using halloc, so anything
|
||||
that should be freed with the job can uset it as a halloc context
|
||||
when allocating.
|
||||
Create a new job.
|
||||
*/
|
||||
job_t *job_create();
|
||||
|
||||
@ -519,7 +517,7 @@ void job_continue( job_t *j, int cont );
|
||||
|
||||
\param interactive whether interactive jobs should be reaped as well
|
||||
*/
|
||||
int job_reap( int interactive );
|
||||
int job_reap( bool interactive );
|
||||
|
||||
/**
|
||||
Signal handler for SIGCHLD. Mark any processes with relevant
|
||||
|
73
reader.cpp
73
reader.cpp
@ -1283,15 +1283,67 @@ static void run_pager( wchar_t *prefix, int is_quoted, const std::vector<complet
|
||||
io_buffer_destroy( in);
|
||||
}
|
||||
|
||||
struct autosuggestion_context_t {
|
||||
history_search_t searcher;
|
||||
file_detection_context_t detector;
|
||||
|
||||
autosuggestion_context_t(history_t *history, const wcstring &term) :
|
||||
searcher(*history, term, HISTORY_SEARCH_TYPE_PREFIX),
|
||||
detector(history, term)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
static int threaded_autosuggest(autosuggestion_context_t *ctx) {
|
||||
ASSERT_IS_BACKGROUND_THREAD();
|
||||
while (ctx->searcher.go_backwards()) {
|
||||
history_item_t item = ctx->searcher.current_item();
|
||||
/* See if the item has any required paths */
|
||||
bool item_ok;
|
||||
const path_list_t &paths = item.get_required_paths();
|
||||
if (paths.empty()) {
|
||||
item_ok = true;
|
||||
} else {
|
||||
ctx->detector.potential_paths = paths;
|
||||
item_ok = ctx->detector.paths_are_valid(paths);
|
||||
}
|
||||
if (item_ok)
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool can_autosuggest(void) {
|
||||
return ! data->suppress_autosuggestion && ! data->command_line.empty() && data->history_search.is_at_end();
|
||||
}
|
||||
|
||||
static void autosuggest_completed(autosuggestion_context_t *ctx, int result) {
|
||||
if (result && can_autosuggest() && ctx->searcher.get_term() == data->command_line) {
|
||||
/* Autosuggestion is active and the search term has not changed, so we're good to go */
|
||||
data->autosuggestion = ctx->searcher.current_string();
|
||||
}
|
||||
delete ctx;
|
||||
}
|
||||
|
||||
|
||||
static void update_autosuggestion(void) {
|
||||
/* Updates autosuggestion. We look for an autosuggestion if the command line is non-empty and if we're not doing a history search. */
|
||||
#if 0
|
||||
/* Old non-threaded mode */
|
||||
data->autosuggestion.clear();
|
||||
if (! data->suppress_autosuggestion && ! data->command_line.empty() && data->history_search.is_at_end()) {
|
||||
if (can_autosuggest()) {
|
||||
history_search_t searcher = history_search_t(*data->history, data->command_line, HISTORY_SEARCH_TYPE_PREFIX);
|
||||
if (searcher.go_backwards()) {
|
||||
data->autosuggestion = searcher.current_item();
|
||||
}
|
||||
}
|
||||
#else
|
||||
data->autosuggestion.clear();
|
||||
if (! data->suppress_autosuggestion && ! data->command_line.empty() && data->history_search.is_at_end()) {
|
||||
autosuggestion_context_t *ctx = new autosuggestion_context_t(data->history, data->command_line);
|
||||
iothread_perform(threaded_autosuggest, autosuggest_completed, ctx);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1317,8 +1369,6 @@ static void reader_flash()
|
||||
reader_super_highlight_me_plenty( data->buff_pos, 0 );
|
||||
|
||||
reader_repaint();
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1853,8 +1903,8 @@ static void handle_token_history( int forward, int reset )
|
||||
Search for previous item that contains this substring
|
||||
*/
|
||||
if (data->history_search.go_backwards()) {
|
||||
wcstring item = data->history_search.current_item();
|
||||
data->token_history_buff = data->history_search.current_item();
|
||||
wcstring item = data->history_search.current_string();
|
||||
data->token_history_buff = data->history_search.current_string();
|
||||
}
|
||||
current_pos = data->token_history_buff.size();
|
||||
|
||||
@ -2435,7 +2485,14 @@ static void reader_super_highlight_me_plenty( int match_highlight_pos, array_lis
|
||||
data->highlight_function( ctx->buff, ctx->color, match_highlight_pos, error, highlight_complete2, ctx );
|
||||
#endif
|
||||
highlight_search();
|
||||
update_autosuggestion();
|
||||
|
||||
/* Here's a hack. Check to see if our autosuggestion still applies; if so, don't recompute it. Since the autosuggestion computation is asynchronous, this avoids "flashing" as you type into the autosuggestion. */
|
||||
const wcstring &cmd = data->command_line, &suggest = data->autosuggestion;
|
||||
if (! suggest.empty() && ! cmd.empty() && string_prefixes_string(cmd, suggest)) {
|
||||
/* The autosuggestion is still reasonable, so do nothing */
|
||||
} else {
|
||||
update_autosuggestion();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -3019,7 +3076,7 @@ const wchar_t *reader_readline()
|
||||
if( ! data->command_line.empty() )
|
||||
{
|
||||
if (data->history) {
|
||||
data->history->add(data->command_line);
|
||||
//data->history->add(data->command_line);
|
||||
data->history->add_with_file_detection(data->command_line);
|
||||
}
|
||||
}
|
||||
@ -3108,7 +3165,7 @@ const wchar_t *reader_readline()
|
||||
if (data->history_search.is_at_end()) {
|
||||
new_text = data->search_buff;
|
||||
} else {
|
||||
new_text = data->history_search.current_item();
|
||||
new_text = data->history_search.current_string();
|
||||
}
|
||||
handle_history(new_text);
|
||||
|
||||
|
13
util.h
13
util.h
@ -449,19 +449,6 @@ void al_foreach( array_list_t *l, void (*func)( void * ));
|
||||
*/
|
||||
void al_foreach2( array_list_t *l, void (*func)( void *, void *), void *aux);
|
||||
|
||||
template<typename T>
|
||||
T al_list_to(array_list_t *list)
|
||||
{
|
||||
T result;
|
||||
int i, c = al_get_count(list);
|
||||
for (i=0; i < c; i++) {
|
||||
void *val = al_get(list, i);
|
||||
result.push_back(val);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Compares two wide character strings with an (arguably) intuitive
|
||||
ordering.
|
||||
|
@ -191,6 +191,12 @@ int waccess(const wchar_t *file_name, int mode)
|
||||
return access(tmp.c_str(), mode);
|
||||
}
|
||||
|
||||
int wunlink(const wchar_t *file_name)
|
||||
{
|
||||
cstring tmp = wcs2string(file_name);
|
||||
return unlink(tmp.c_str());
|
||||
}
|
||||
|
||||
void wperror(const wchar_t *s)
|
||||
{
|
||||
int e = errno;
|
||||
|
Loading…
Reference in New Issue
Block a user