You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
226 lines
6.3 KiB
226 lines
6.3 KiB
"""
|
|
Implementations for the history of a `Buffer`.
|
|
|
|
NOTE: Notice that there is no `DynamicHistory`. This doesn't work well, because
|
|
the `Buffer` needs to be able to attach an event handler to the event
|
|
when a history entry is loaded. This loading can be done asynchronously
|
|
and making the history swappable would probably break this.
|
|
"""
|
|
from __future__ import unicode_literals
|
|
|
|
from .utils import Event
|
|
from .eventloop import AsyncGeneratorItem, From, ensure_future, consume_async_generator, generator_to_async_generator
|
|
|
|
from abc import ABCMeta, abstractmethod
|
|
from six import with_metaclass, text_type
|
|
|
|
import datetime
|
|
import os
|
|
|
|
__all__ = [
|
|
'History',
|
|
'ThreadedHistory',
|
|
'DummyHistory',
|
|
'FileHistory',
|
|
'InMemoryHistory',
|
|
]
|
|
|
|
|
|
class History(with_metaclass(ABCMeta, object)):
|
|
"""
|
|
Base ``History`` class.
|
|
|
|
This also includes abstract methods for loading/storing history.
|
|
"""
|
|
def __init__(self):
|
|
# In memory storage for strings.
|
|
self._loading = False
|
|
self._loaded_strings = []
|
|
self._item_loaded = Event(self)
|
|
|
|
def _start_loading(self):
|
|
"""
|
|
Consume the asynchronous generator: `load_history_strings_async`.
|
|
|
|
This is only called once, because once the history is loaded, we don't
|
|
have to load it again.
|
|
"""
|
|
def add_string(string):
|
|
" Got one string from the asynchronous history generator. "
|
|
self._loaded_strings.insert(0, string)
|
|
self._item_loaded.fire()
|
|
|
|
yield From(consume_async_generator(
|
|
self.load_history_strings_async(),
|
|
cancel=lambda: False, # Right now, we don't have cancellation
|
|
# of history loading in any way.
|
|
item_callback=add_string))
|
|
|
|
#
|
|
# Methods expected by `Buffer`.
|
|
#
|
|
|
|
def start_loading(self):
|
|
" Start loading the history. "
|
|
if not self._loading:
|
|
self._loading = True
|
|
ensure_future(self._start_loading())
|
|
|
|
def get_item_loaded_event(self):
|
|
" Event which is triggered when a new item is loaded. "
|
|
return self._item_loaded
|
|
|
|
def get_strings(self):
|
|
"""
|
|
Get the strings from the history that are loaded so far.
|
|
"""
|
|
return self._loaded_strings
|
|
|
|
def append_string(self, string):
|
|
" Add string to the history. "
|
|
self._loaded_strings.append(string)
|
|
self.store_string(string)
|
|
|
|
#
|
|
# Implementation for specific backends.
|
|
#
|
|
|
|
@abstractmethod
|
|
def load_history_strings(self):
|
|
"""
|
|
This should be a generator that yields `str` instances.
|
|
|
|
It should yield the most recent items first, because they are the most
|
|
important. (The history can already be used, even when it's only
|
|
partially loaded.)
|
|
"""
|
|
while False:
|
|
yield
|
|
|
|
def load_history_strings_async(self):
|
|
"""
|
|
Asynchronous generator for history strings. (Probably, you won't have
|
|
to override this.)
|
|
|
|
This should return an iterable that can yield both `str`
|
|
and `Future` objects. The `str` objects have to be
|
|
wrapped in a `AsyncGeneratorItem` object.
|
|
|
|
If we drop Python 2 support in the future, this could become a true
|
|
asynchronous generator.
|
|
"""
|
|
for item in self.load_history_strings():
|
|
assert isinstance(item, text_type)
|
|
yield AsyncGeneratorItem(item)
|
|
|
|
@abstractmethod
|
|
def store_string(self, string):
|
|
"""
|
|
Store the string in persistent storage.
|
|
"""
|
|
|
|
|
|
class ThreadedHistory(History):
|
|
"""
|
|
Wrapper that runs the `load_history_strings` generator in a thread.
|
|
|
|
Use this to increase the start-up time of prompt_toolkit applications.
|
|
History entries are available as soon as they are loaded. We don't have to
|
|
wait for everything to be loaded.
|
|
"""
|
|
def __init__(self, history=None):
|
|
assert isinstance(history, History), 'Got %r' % (history, )
|
|
self.history = history
|
|
super(ThreadedHistory, self).__init__()
|
|
|
|
def load_history_strings_async(self):
|
|
"""
|
|
Asynchronous generator of completions.
|
|
This yields both Future and Completion objects.
|
|
"""
|
|
return generator_to_async_generator(
|
|
self.history.load_history_strings)
|
|
|
|
# All of the following are proxied to `self.history`.
|
|
|
|
def load_history_strings(self):
|
|
return self.history.load_history_strings()
|
|
|
|
def store_string(self, string):
|
|
self.history.store_string(string)
|
|
|
|
def __repr__(self):
|
|
return 'ThreadedHistory(%r)' % (self.history, )
|
|
|
|
|
|
class InMemoryHistory(History):
|
|
"""
|
|
:class:`.History` class that keeps a list of all strings in memory.
|
|
"""
|
|
def load_history_strings(self):
|
|
return []
|
|
|
|
def store_string(self, string):
|
|
pass
|
|
|
|
|
|
class DummyHistory(History):
|
|
"""
|
|
:class:`.History` object that doesn't remember anything.
|
|
"""
|
|
def load_history_strings(self):
|
|
return []
|
|
|
|
def store_string(self, string):
|
|
pass
|
|
|
|
def append_string(self, string):
|
|
# Don't remember this.
|
|
pass
|
|
|
|
|
|
class FileHistory(History):
|
|
"""
|
|
:class:`.History` class that stores all strings in a file.
|
|
"""
|
|
def __init__(self, filename):
|
|
self.filename = filename
|
|
super(FileHistory, self).__init__()
|
|
|
|
def load_history_strings(self):
|
|
strings = []
|
|
lines = []
|
|
|
|
def add():
|
|
if lines:
|
|
# Join and drop trailing newline.
|
|
string = ''.join(lines)[:-1]
|
|
|
|
strings.append(string)
|
|
|
|
if os.path.exists(self.filename):
|
|
with open(self.filename, 'rb') as f:
|
|
for line in f:
|
|
line = line.decode('utf-8')
|
|
|
|
if line.startswith('+'):
|
|
lines.append(line[1:])
|
|
else:
|
|
add()
|
|
lines = []
|
|
|
|
add()
|
|
|
|
# Reverse the order, because newest items have to go first.
|
|
return reversed(strings)
|
|
|
|
def store_string(self, string):
|
|
# Save to file.
|
|
with open(self.filename, 'ab') as f:
|
|
def write(t):
|
|
f.write(t.encode('utf-8'))
|
|
|
|
write('\n# %s\n' % datetime.datetime.now())
|
|
for line in string.split('\n'):
|
|
write('+%s\n' % line)
|