• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#   Copyright 2000-2004 Michael Hudson-Doyle <micahel@gmail.com>
2#
3#                        All Rights Reserved
4#
5#
6# Permission to use, copy, modify, and distribute this software and
7# its documentation for any purpose is hereby granted without fee,
8# provided that the above copyright notice appear in all copies and
9# that both that copyright notice and this permission notice appear in
10# supporting documentation.
11#
12# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
13# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
14# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
15# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
16# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
17# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
18# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
19
20from __future__ import annotations
21
22from contextlib import contextmanager
23from dataclasses import dataclass, field
24
25from . import commands, input
26from .reader import Reader
27
28
29if False:
30    from .types import SimpleContextManager, KeySpec, CommandName
31
32
33isearch_keymap: tuple[tuple[KeySpec, CommandName], ...] = tuple(
34    [("\\%03o" % c, "isearch-end") for c in range(256) if chr(c) != "\\"]
35    + [(c, "isearch-add-character") for c in map(chr, range(32, 127)) if c != "\\"]
36    + [
37        ("\\%03o" % c, "isearch-add-character")
38        for c in range(256)
39        if chr(c).isalpha() and chr(c) != "\\"
40    ]
41    + [
42        ("\\\\", "self-insert"),
43        (r"\C-r", "isearch-backwards"),
44        (r"\C-s", "isearch-forwards"),
45        (r"\C-c", "isearch-cancel"),
46        (r"\C-g", "isearch-cancel"),
47        (r"\<backspace>", "isearch-backspace"),
48    ]
49)
50
51ISEARCH_DIRECTION_NONE = ""
52ISEARCH_DIRECTION_BACKWARDS = "r"
53ISEARCH_DIRECTION_FORWARDS = "f"
54
55
56class next_history(commands.Command):
57    def do(self) -> None:
58        r = self.reader
59        if r.historyi == len(r.history):
60            r.error("end of history list")
61            return
62        r.select_item(r.historyi + 1)
63
64
65class previous_history(commands.Command):
66    def do(self) -> None:
67        r = self.reader
68        if r.historyi == 0:
69            r.error("start of history list")
70            return
71        r.select_item(r.historyi - 1)
72
73
74class history_search_backward(commands.Command):
75    def do(self) -> None:
76        r = self.reader
77        r.search_next(forwards=False)
78
79
80class history_search_forward(commands.Command):
81    def do(self) -> None:
82        r = self.reader
83        r.search_next(forwards=True)
84
85
86class restore_history(commands.Command):
87    def do(self) -> None:
88        r = self.reader
89        if r.historyi != len(r.history):
90            if r.get_unicode() != r.history[r.historyi]:
91                r.buffer = list(r.history[r.historyi])
92                r.pos = len(r.buffer)
93                r.dirty = True
94
95
96class first_history(commands.Command):
97    def do(self) -> None:
98        self.reader.select_item(0)
99
100
101class last_history(commands.Command):
102    def do(self) -> None:
103        self.reader.select_item(len(self.reader.history))
104
105
106class operate_and_get_next(commands.FinishCommand):
107    def do(self) -> None:
108        self.reader.next_history = self.reader.historyi + 1
109
110
111class yank_arg(commands.Command):
112    def do(self) -> None:
113        r = self.reader
114        if r.last_command is self.__class__:
115            r.yank_arg_i += 1
116        else:
117            r.yank_arg_i = 0
118        if r.historyi < r.yank_arg_i:
119            r.error("beginning of history list")
120            return
121        a = r.get_arg(-1)
122        # XXX how to split?
123        words = r.get_item(r.historyi - r.yank_arg_i - 1).split()
124        if a < -len(words) or a >= len(words):
125            r.error("no such arg")
126            return
127        w = words[a]
128        b = r.buffer
129        if r.yank_arg_i > 0:
130            o = len(r.yank_arg_yanked)
131        else:
132            o = 0
133        b[r.pos - o : r.pos] = list(w)
134        r.yank_arg_yanked = w
135        r.pos += len(w) - o
136        r.dirty = True
137
138
139class forward_history_isearch(commands.Command):
140    def do(self) -> None:
141        r = self.reader
142        r.isearch_direction = ISEARCH_DIRECTION_FORWARDS
143        r.isearch_start = r.historyi, r.pos
144        r.isearch_term = ""
145        r.dirty = True
146        r.push_input_trans(r.isearch_trans)
147
148
149class reverse_history_isearch(commands.Command):
150    def do(self) -> None:
151        r = self.reader
152        r.isearch_direction = ISEARCH_DIRECTION_BACKWARDS
153        r.dirty = True
154        r.isearch_term = ""
155        r.push_input_trans(r.isearch_trans)
156        r.isearch_start = r.historyi, r.pos
157
158
159class isearch_cancel(commands.Command):
160    def do(self) -> None:
161        r = self.reader
162        r.isearch_direction = ISEARCH_DIRECTION_NONE
163        r.pop_input_trans()
164        r.select_item(r.isearch_start[0])
165        r.pos = r.isearch_start[1]
166        r.dirty = True
167
168
169class isearch_add_character(commands.Command):
170    def do(self) -> None:
171        r = self.reader
172        b = r.buffer
173        r.isearch_term += self.event[-1]
174        r.dirty = True
175        p = r.pos + len(r.isearch_term) - 1
176        if b[p : p + 1] != [r.isearch_term[-1]]:
177            r.isearch_next()
178
179
180class isearch_backspace(commands.Command):
181    def do(self) -> None:
182        r = self.reader
183        if len(r.isearch_term) > 0:
184            r.isearch_term = r.isearch_term[:-1]
185            r.dirty = True
186        else:
187            r.error("nothing to rubout")
188
189
190class isearch_forwards(commands.Command):
191    def do(self) -> None:
192        r = self.reader
193        r.isearch_direction = ISEARCH_DIRECTION_FORWARDS
194        r.isearch_next()
195
196
197class isearch_backwards(commands.Command):
198    def do(self) -> None:
199        r = self.reader
200        r.isearch_direction = ISEARCH_DIRECTION_BACKWARDS
201        r.isearch_next()
202
203
204class isearch_end(commands.Command):
205    def do(self) -> None:
206        r = self.reader
207        r.isearch_direction = ISEARCH_DIRECTION_NONE
208        r.console.forgetinput()
209        r.pop_input_trans()
210        r.dirty = True
211
212
213@dataclass
214class HistoricalReader(Reader):
215    """Adds history support (with incremental history searching) to the
216    Reader class.
217    """
218
219    history: list[str] = field(default_factory=list)
220    historyi: int = 0
221    next_history: int | None = None
222    transient_history: dict[int, str] = field(default_factory=dict)
223    isearch_term: str = ""
224    isearch_direction: str = ISEARCH_DIRECTION_NONE
225    isearch_start: tuple[int, int] = field(init=False)
226    isearch_trans: input.KeymapTranslator = field(init=False)
227    yank_arg_i: int = 0
228    yank_arg_yanked: str = ""
229
230    def __post_init__(self) -> None:
231        super().__post_init__()
232        for c in [
233            next_history,
234            previous_history,
235            restore_history,
236            first_history,
237            last_history,
238            yank_arg,
239            forward_history_isearch,
240            reverse_history_isearch,
241            isearch_end,
242            isearch_add_character,
243            isearch_cancel,
244            isearch_add_character,
245            isearch_backspace,
246            isearch_forwards,
247            isearch_backwards,
248            operate_and_get_next,
249            history_search_backward,
250            history_search_forward,
251        ]:
252            self.commands[c.__name__] = c
253            self.commands[c.__name__.replace("_", "-")] = c
254        self.isearch_start = self.historyi, self.pos
255        self.isearch_trans = input.KeymapTranslator(
256            isearch_keymap, invalid_cls=isearch_end, character_cls=isearch_add_character
257        )
258
259    def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]:
260        return super().collect_keymap() + (
261            (r"\C-n", "next-history"),
262            (r"\C-p", "previous-history"),
263            (r"\C-o", "operate-and-get-next"),
264            (r"\C-r", "reverse-history-isearch"),
265            (r"\C-s", "forward-history-isearch"),
266            (r"\M-r", "restore-history"),
267            (r"\M-.", "yank-arg"),
268            (r"\<page down>", "history-search-forward"),
269            (r"\x1b[6~", "history-search-forward"),
270            (r"\<page up>", "history-search-backward"),
271            (r"\x1b[5~", "history-search-backward"),
272        )
273
274    def select_item(self, i: int) -> None:
275        self.transient_history[self.historyi] = self.get_unicode()
276        buf = self.transient_history.get(i)
277        if buf is None:
278            buf = self.history[i].rstrip()
279        self.buffer = list(buf)
280        self.historyi = i
281        self.pos = len(self.buffer)
282        self.dirty = True
283        self.last_refresh_cache.invalidated = True
284
285    def get_item(self, i: int) -> str:
286        if i != len(self.history):
287            return self.transient_history.get(i, self.history[i])
288        else:
289            return self.transient_history.get(i, self.get_unicode())
290
291    @contextmanager
292    def suspend(self) -> SimpleContextManager:
293        with super().suspend():
294            try:
295                old_history = self.history[:]
296                del self.history[:]
297                yield
298            finally:
299                self.history[:] = old_history
300
301    def prepare(self) -> None:
302        super().prepare()
303        try:
304            self.transient_history = {}
305            if self.next_history is not None and self.next_history < len(self.history):
306                self.historyi = self.next_history
307                self.buffer[:] = list(self.history[self.next_history])
308                self.pos = len(self.buffer)
309                self.transient_history[len(self.history)] = ""
310            else:
311                self.historyi = len(self.history)
312            self.next_history = None
313        except:
314            self.restore()
315            raise
316
317    def get_prompt(self, lineno: int, cursor_on_line: bool) -> str:
318        if cursor_on_line and self.isearch_direction != ISEARCH_DIRECTION_NONE:
319            d = "rf"[self.isearch_direction == ISEARCH_DIRECTION_FORWARDS]
320            return "(%s-search `%s') " % (d, self.isearch_term)
321        else:
322            return super().get_prompt(lineno, cursor_on_line)
323
324    def search_next(self, *, forwards: bool) -> None:
325        """Search history for the current line contents up to the cursor.
326
327        Selects the first item found. If nothing is under the cursor, any next
328        item in history is selected.
329        """
330        pos = self.pos
331        s = self.get_unicode()
332        history_index = self.historyi
333
334        # In multiline contexts, we're only interested in the current line.
335        nl_index = s.rfind('\n', 0, pos)
336        prefix = s[nl_index + 1:pos]
337        pos = len(prefix)
338
339        match_prefix = len(prefix)
340        len_item = 0
341        if history_index < len(self.history):
342            len_item = len(self.get_item(history_index))
343        if len_item and pos == len_item:
344            match_prefix = False
345        elif not pos:
346            match_prefix = False
347
348        while 1:
349            if forwards:
350                out_of_bounds = history_index >= len(self.history) - 1
351            else:
352                out_of_bounds = history_index == 0
353            if out_of_bounds:
354                if forwards and not match_prefix:
355                    self.pos = 0
356                    self.buffer = []
357                    self.dirty = True
358                else:
359                    self.error("not found")
360                return
361
362            history_index += 1 if forwards else -1
363            s = self.get_item(history_index)
364
365            if not match_prefix:
366                self.select_item(history_index)
367                return
368
369            len_acc = 0
370            for i, line in enumerate(s.splitlines(keepends=True)):
371                if line.startswith(prefix):
372                    self.select_item(history_index)
373                    self.pos = pos + len_acc
374                    return
375                len_acc += len(line)
376
377    def isearch_next(self) -> None:
378        st = self.isearch_term
379        p = self.pos
380        i = self.historyi
381        s = self.get_unicode()
382        forwards = self.isearch_direction == ISEARCH_DIRECTION_FORWARDS
383        while 1:
384            if forwards:
385                p = s.find(st, p + 1)
386            else:
387                p = s.rfind(st, 0, p + len(st) - 1)
388            if p != -1:
389                self.select_item(i)
390                self.pos = p
391                return
392            elif (forwards and i >= len(self.history) - 1) or (not forwards and i == 0):
393                self.error("not found")
394                return
395            else:
396                if forwards:
397                    i += 1
398                    s = self.get_item(i)
399                    p = -1
400                else:
401                    i -= 1
402                    s = self.get_item(i)
403                    p = len(s)
404
405    def finish(self) -> None:
406        super().finish()
407        ret = self.get_unicode()
408        for i, t in self.transient_history.items():
409            if i < len(self.history) and i != self.historyi:
410                self.history[i] = t
411        if ret and should_auto_add_history:
412            self.history.append(ret)
413
414
415should_auto_add_history = True
416