euporie 2.8.6__py3-none-any.whl → 2.8.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- euporie/console/app.py +2 -0
- euporie/console/tabs/console.py +27 -17
- euporie/core/__init__.py +2 -2
- euporie/core/app/_commands.py +4 -21
- euporie/core/app/app.py +13 -7
- euporie/core/bars/command.py +9 -6
- euporie/core/bars/search.py +43 -2
- euporie/core/border.py +7 -2
- euporie/core/comm/base.py +2 -2
- euporie/core/comm/ipywidgets.py +3 -3
- euporie/core/commands.py +44 -8
- euporie/core/completion.py +14 -6
- euporie/core/convert/datum.py +7 -7
- euporie/core/data_structures.py +20 -1
- euporie/core/filters.py +8 -0
- euporie/core/ft/html.py +47 -40
- euporie/core/graphics.py +11 -3
- euporie/core/history.py +15 -5
- euporie/core/inspection.py +16 -9
- euporie/core/kernel/__init__.py +53 -1
- euporie/core/kernel/base.py +571 -0
- euporie/core/kernel/{client.py → jupyter.py} +173 -430
- euporie/core/kernel/{manager.py → jupyter_manager.py} +4 -3
- euporie/core/kernel/local.py +694 -0
- euporie/core/key_binding/bindings/basic.py +6 -3
- euporie/core/keys.py +26 -25
- euporie/core/layout/cache.py +31 -7
- euporie/core/layout/containers.py +88 -13
- euporie/core/layout/scroll.py +45 -148
- euporie/core/log.py +1 -1
- euporie/core/style.py +2 -1
- euporie/core/suggest.py +155 -74
- euporie/core/tabs/__init__.py +10 -0
- euporie/core/tabs/_commands.py +76 -0
- euporie/core/tabs/_settings.py +16 -0
- euporie/core/tabs/base.py +22 -8
- euporie/core/tabs/kernel.py +81 -35
- euporie/core/tabs/notebook.py +14 -22
- euporie/core/utils.py +1 -1
- euporie/core/validation.py +8 -8
- euporie/core/widgets/_settings.py +19 -2
- euporie/core/widgets/cell.py +31 -31
- euporie/core/widgets/cell_outputs.py +10 -1
- euporie/core/widgets/dialog.py +30 -75
- euporie/core/widgets/forms.py +71 -59
- euporie/core/widgets/inputs.py +7 -4
- euporie/core/widgets/layout.py +281 -93
- euporie/core/widgets/menu.py +55 -15
- euporie/core/widgets/palette.py +3 -1
- euporie/core/widgets/tree.py +86 -76
- euporie/notebook/app.py +35 -16
- euporie/notebook/tabs/edit.py +4 -4
- euporie/notebook/tabs/json.py +6 -2
- euporie/notebook/tabs/notebook.py +26 -8
- euporie/preview/tabs/notebook.py +17 -13
- euporie/web/tabs/web.py +22 -3
- euporie/web/widgets/webview.py +3 -0
- {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/METADATA +1 -1
- {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/RECORD +64 -61
- {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/entry_points.txt +1 -1
- {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/licenses/LICENSE +1 -1
- {euporie-2.8.6.data → euporie-2.8.7.data}/data/share/applications/euporie-console.desktop +0 -0
- {euporie-2.8.6.data → euporie-2.8.7.data}/data/share/applications/euporie-notebook.desktop +0 -0
- {euporie-2.8.6.dist-info → euporie-2.8.7.dist-info}/WHEEL +0 -0
euporie/core/layout/scroll.py
CHANGED
@@ -7,13 +7,13 @@ import logging
|
|
7
7
|
from typing import TYPE_CHECKING, cast
|
8
8
|
|
9
9
|
from prompt_toolkit.application.current import get_app
|
10
|
+
from prompt_toolkit.cache import FastDictCache
|
10
11
|
from prompt_toolkit.filters import is_searching
|
11
12
|
from prompt_toolkit.layout.containers import (
|
12
13
|
Container,
|
13
14
|
ScrollOffsets,
|
14
15
|
Window,
|
15
16
|
WindowRenderInfo,
|
16
|
-
to_container,
|
17
17
|
)
|
18
18
|
from prompt_toolkit.layout.controls import UIContent
|
19
19
|
from prompt_toolkit.layout.dimension import Dimension, to_dimension
|
@@ -26,10 +26,7 @@ if TYPE_CHECKING:
|
|
26
26
|
from collections.abc import Sequence
|
27
27
|
from typing import Callable, Literal
|
28
28
|
|
29
|
-
from prompt_toolkit.key_binding.key_bindings import
|
30
|
-
KeyBindingsBase,
|
31
|
-
NotImplementedOrNone,
|
32
|
-
)
|
29
|
+
from prompt_toolkit.key_binding.key_bindings import NotImplementedOrNone
|
33
30
|
from prompt_toolkit.layout.containers import AnyContainer
|
34
31
|
from prompt_toolkit.layout.dimension import AnyDimension
|
35
32
|
from prompt_toolkit.layout.mouse_handlers import MouseHandlers
|
@@ -67,6 +64,9 @@ class ScrollingContainer(Container):
|
|
67
64
|
self.children_func = _children_func
|
68
65
|
self._child_cache: dict[int, CachedContainer] = {}
|
69
66
|
self._children: list[CachedContainer] = []
|
67
|
+
self._known_sizes_cache: FastDictCache[tuple[int], list[int]] = FastDictCache(
|
68
|
+
self._known_sizes
|
69
|
+
)
|
70
70
|
self.refresh_children = True
|
71
71
|
self.pre_rendered: float | None = None
|
72
72
|
|
@@ -79,6 +79,7 @@ class ScrollingContainer(Container):
|
|
79
79
|
self.index_positions: dict[int, int | None] = {}
|
80
80
|
|
81
81
|
self.last_write_position: WritePosition = BoundedWritePosition(0, 0, 0, 0)
|
82
|
+
self.last_total_height = 0
|
82
83
|
|
83
84
|
self.width = to_dimension(width).preferred
|
84
85
|
self.height = to_dimension(height).preferred
|
@@ -92,8 +93,10 @@ class ScrollingContainer(Container):
|
|
92
93
|
|
93
94
|
def pre_render_children(self, width: int, height: int) -> None:
|
94
95
|
"""Render all unrendered children in a background thread."""
|
95
|
-
self.pre_rendered = 0.0
|
96
96
|
children = self.all_children()
|
97
|
+
if not children:
|
98
|
+
return
|
99
|
+
self.pre_rendered = 0.0
|
97
100
|
incr = 1 / len(children)
|
98
101
|
app = get_app()
|
99
102
|
|
@@ -264,8 +267,8 @@ class ScrollingContainer(Container):
|
|
264
267
|
if bottom_pos is not None:
|
265
268
|
n = max(
|
266
269
|
n,
|
267
|
-
|
268
|
-
- self.
|
270
|
+
self.last_write_position.height
|
271
|
+
- (bottom_pos + bottom_child.height + self.scrolling),
|
269
272
|
)
|
270
273
|
if (
|
271
274
|
bottom_pos + bottom_child.height + self.scrolling + n
|
@@ -362,6 +365,10 @@ class ScrollingContainer(Container):
|
|
362
365
|
their style down to the windows that they contain.
|
363
366
|
z_index: Used for propagating z_index from parent to child.
|
364
367
|
"""
|
368
|
+
# Record where the container was last drawn so we can determine if cell outputs
|
369
|
+
# are partially obscured
|
370
|
+
self.last_write_position = write_position
|
371
|
+
|
365
372
|
ypos = write_position.ypos
|
366
373
|
xpos = write_position.xpos
|
367
374
|
|
@@ -415,12 +422,9 @@ class ScrollingContainer(Container):
|
|
415
422
|
self.scroll_to_cursor = False
|
416
423
|
|
417
424
|
# Adjust scrolling offset
|
418
|
-
|
419
|
-
|
420
|
-
|
421
|
-
self.get_child(index).height or 1
|
422
|
-
for index in range(len(self._children))
|
423
|
-
]
|
425
|
+
heights = self.known_sizes
|
426
|
+
total_height = sum(heights)
|
427
|
+
if self.scrolling or total_height != self.last_total_height:
|
424
428
|
heights_above = sum(heights[: self._selected_slice.start])
|
425
429
|
new_child_position = self.selected_child_position + self.scrolling
|
426
430
|
# Do not allow scrolling if there is no overflow
|
@@ -442,6 +446,8 @@ class ScrollingContainer(Container):
|
|
442
446
|
self.scrolling = min(0, self.scrolling - underscroll)
|
443
447
|
self.selected_child_position += self.scrolling
|
444
448
|
|
449
|
+
self.last_total_height = total_height
|
450
|
+
|
445
451
|
# Blit first selected child and those below it that are on screen
|
446
452
|
line = self.selected_child_position
|
447
453
|
for i in range(self._selected_slice.start, len(self._children)):
|
@@ -551,14 +557,10 @@ class ScrollingContainer(Container):
|
|
551
557
|
|
552
558
|
_walk(self)
|
553
559
|
|
554
|
-
# Record where the contain was last drawn so we can determine if cell outputs
|
555
|
-
# are partially obscured
|
556
|
-
self.last_write_position = write_position
|
557
|
-
|
558
560
|
# Mock up a WindowRenderInfo so we can draw a scrollbar margin
|
559
561
|
self.render_info = WindowRenderInfo(
|
560
562
|
window=cast("Window", self),
|
561
|
-
ui_content=UIContent(line_count=max(sum(
|
563
|
+
ui_content=UIContent(line_count=max(sum(heights), 1)),
|
562
564
|
horizontal_scroll=0,
|
563
565
|
vertical_scroll=self.vertical_scroll,
|
564
566
|
window_width=available_width,
|
@@ -577,11 +579,33 @@ class ScrollingContainer(Container):
|
|
577
579
|
if self.pre_rendered is None:
|
578
580
|
self.pre_render_children(available_width, available_height)
|
579
581
|
|
582
|
+
@property
|
583
|
+
def known_sizes(self) -> list[int]:
|
584
|
+
"""Map of child indices to height values."""
|
585
|
+
return self._known_sizes_cache[get_app().render_counter,]
|
586
|
+
|
587
|
+
def _known_sizes(self, render_counter: int) -> list[int]:
|
588
|
+
"""Calculate sizes of children once per render cycle."""
|
589
|
+
sizes = {}
|
590
|
+
missing = set()
|
591
|
+
available_width = self.last_write_position.width
|
592
|
+
available_height = self.last_write_position.height
|
593
|
+
for i, child in enumerate(self._children):
|
594
|
+
if isinstance(child, CachedContainer) and child.height:
|
595
|
+
sizes[i] = child.preferred_height(
|
596
|
+
available_width, available_height
|
597
|
+
).preferred
|
598
|
+
else:
|
599
|
+
missing.add(i)
|
600
|
+
avg = int(sum(sizes.values()) / (len(sizes) or 1))
|
601
|
+
sizes.update(dict.fromkeys(missing, avg))
|
602
|
+
return [v for k, v in sorted(sizes.items())]
|
603
|
+
|
580
604
|
@property
|
581
605
|
def vertical_scroll(self) -> int:
|
582
606
|
"""The best guess at the absolute vertical scroll position."""
|
583
607
|
return (
|
584
|
-
sum(
|
608
|
+
sum(self.known_sizes[: self._selected_slice.start])
|
585
609
|
- self.selected_child_position
|
586
610
|
)
|
587
611
|
|
@@ -589,7 +613,7 @@ class ScrollingContainer(Container):
|
|
589
613
|
def vertical_scroll(self, value: int) -> None:
|
590
614
|
"""Set the absolute vertical scroll position."""
|
591
615
|
self.selected_child_position = (
|
592
|
-
sum(
|
616
|
+
sum(self.known_sizes[: self._selected_slice.start]) - value
|
593
617
|
)
|
594
618
|
|
595
619
|
def all_children(self) -> Sequence[Container]:
|
@@ -709,34 +733,6 @@ class ScrollingContainer(Container):
|
|
709
733
|
else:
|
710
734
|
self.selected_child_position = new_top
|
711
735
|
|
712
|
-
heights = [
|
713
|
-
self.get_child(index).height or 1 for index in range(len(self._children))
|
714
|
-
]
|
715
|
-
# Do not allow bottom child to scroll above screen bottom
|
716
|
-
self.selected_child_position += max(
|
717
|
-
0,
|
718
|
-
self.last_write_position.height
|
719
|
-
- (self.selected_child_position + sum(heights[index:])),
|
720
|
-
)
|
721
|
-
# Do not allow top child to scroll below screen top
|
722
|
-
self.selected_child_position -= max(
|
723
|
-
0, self.selected_child_position - sum(heights[:index])
|
724
|
-
)
|
725
|
-
|
726
|
-
@property
|
727
|
-
def known_sizes(self) -> dict[int, int]:
|
728
|
-
"""A dictionary mapping child indices to height values."""
|
729
|
-
sizes = {}
|
730
|
-
missing = set()
|
731
|
-
for i, child in enumerate(self._children):
|
732
|
-
if isinstance(child, CachedContainer) and child.height:
|
733
|
-
sizes[i] = child.height
|
734
|
-
else:
|
735
|
-
missing.add(i)
|
736
|
-
avg = int(sum(sizes.values()) / len(sizes))
|
737
|
-
sizes.update(dict.fromkeys(missing, avg))
|
738
|
-
return sizes
|
739
|
-
|
740
736
|
def _scroll_up(self) -> None:
|
741
737
|
"""Scroll up one line: for compatibility with :py:class:`Window`."""
|
742
738
|
self.scroll(1)
|
@@ -744,102 +740,3 @@ class ScrollingContainer(Container):
|
|
744
740
|
def _scroll_down(self) -> None:
|
745
741
|
"""Scroll down one line: for compatibility with :py:class:`Window`."""
|
746
742
|
self.scroll(-1)
|
747
|
-
|
748
|
-
|
749
|
-
class PrintingContainer(Container):
|
750
|
-
"""A container which displays all it's children in a vertical list."""
|
751
|
-
|
752
|
-
def __init__(
|
753
|
-
self,
|
754
|
-
children: Callable | Sequence[AnyContainer],
|
755
|
-
width: AnyDimension = None,
|
756
|
-
key_bindings: KeyBindingsBase | None = None,
|
757
|
-
) -> None:
|
758
|
-
"""Initiate the container."""
|
759
|
-
self.width = width
|
760
|
-
self.rendered = False
|
761
|
-
self._children = children
|
762
|
-
self.key_bindings = key_bindings
|
763
|
-
|
764
|
-
def get_key_bindings(self) -> KeyBindingsBase | None:
|
765
|
-
"""Return the container's key bindings."""
|
766
|
-
return self.key_bindings
|
767
|
-
|
768
|
-
@property
|
769
|
-
def children(self) -> Sequence[AnyContainer]:
|
770
|
-
"""Return the container's children."""
|
771
|
-
children = self._children() if callable(self._children) else self._children
|
772
|
-
return children or [Window()]
|
773
|
-
|
774
|
-
def get_children(self) -> list[Container]:
|
775
|
-
"""Return a list of all child containers."""
|
776
|
-
return [to_container(child) for child in self.children]
|
777
|
-
|
778
|
-
def write_to_screen(
|
779
|
-
self,
|
780
|
-
screen: PtkScreen,
|
781
|
-
mouse_handlers: MouseHandlers,
|
782
|
-
write_position: WritePosition,
|
783
|
-
parent_style: str,
|
784
|
-
erase_bg: bool,
|
785
|
-
z_index: int | None,
|
786
|
-
) -> None:
|
787
|
-
"""Render the container to a `Screen` instance.
|
788
|
-
|
789
|
-
All children are rendered vertically in sequence.
|
790
|
-
|
791
|
-
Args:
|
792
|
-
screen: The :class:`~prompt_toolkit.layout.screen.Screen` class to which
|
793
|
-
the output has to be written.
|
794
|
-
mouse_handlers: :class:`prompt_toolkit.layout.mouse_handlers.MouseHandlers`.
|
795
|
-
write_position: A :class:`prompt_toolkit.layout.screen.WritePosition` object
|
796
|
-
defining where this container should be drawn.
|
797
|
-
erase_bg: If true, the background will be erased prior to drawing.
|
798
|
-
parent_style: Style string to pass to the :class:`.Window` object. This will
|
799
|
-
be applied to all content of the windows. :class:`.VSplit` and
|
800
|
-
:class:`prompt_toolkit.layout.containers.HSplit` can use it to pass
|
801
|
-
their style down to the windows that they contain.
|
802
|
-
z_index: Used for propagating z_index from parent to child.
|
803
|
-
|
804
|
-
"""
|
805
|
-
xpos = write_position.xpos
|
806
|
-
ypos = write_position.ypos
|
807
|
-
|
808
|
-
children = self.get_children()
|
809
|
-
for child in children:
|
810
|
-
height = child.preferred_height(write_position.width, 999999).preferred
|
811
|
-
child.write_to_screen(
|
812
|
-
screen,
|
813
|
-
mouse_handlers,
|
814
|
-
BoundedWritePosition(xpos, ypos, write_position.width, height),
|
815
|
-
parent_style,
|
816
|
-
erase_bg,
|
817
|
-
z_index,
|
818
|
-
)
|
819
|
-
ypos += height
|
820
|
-
|
821
|
-
def preferred_height(self, width: int, max_available_height: int) -> Dimension:
|
822
|
-
"""Return the preferred height, equal to the sum of the child heights."""
|
823
|
-
return Dimension(
|
824
|
-
min=1,
|
825
|
-
preferred=sum(
|
826
|
-
[
|
827
|
-
c.preferred_height(width, max_available_height).preferred
|
828
|
-
for c in self.get_children()
|
829
|
-
]
|
830
|
-
),
|
831
|
-
)
|
832
|
-
|
833
|
-
def preferred_width(self, max_available_width: int) -> Dimension:
|
834
|
-
"""Calculate and returns the desired width for this container."""
|
835
|
-
if self.width is not None:
|
836
|
-
dim = to_dimension(self.width).preferred
|
837
|
-
return Dimension(max=dim, preferred=dim)
|
838
|
-
else:
|
839
|
-
return Dimension(max_available_width)
|
840
|
-
|
841
|
-
def reset(self) -> None:
|
842
|
-
"""Reet the state of this container and all the children.
|
843
|
-
|
844
|
-
Does nothing as this container is used for dumping output.
|
845
|
-
"""
|
euporie/core/log.py
CHANGED
@@ -340,7 +340,7 @@ class StdoutFormatter(FtFormatter):
|
|
340
340
|
|
341
341
|
|
342
342
|
class stdout_to_log:
|
343
|
-
"""A
|
343
|
+
"""A context manager which captures standard output and logs it."""
|
344
344
|
|
345
345
|
def __init__(
|
346
346
|
self, log: logging.Logger, output: str = "Literal['stdout','stderr']"
|
euporie/core/style.py
CHANGED
@@ -141,6 +141,7 @@ IPYWIDGET_STYLE = [
|
|
141
141
|
# ("ipywidget danger border right selection", "fg:ansibrightred"),
|
142
142
|
# ("ipywidget danger border bottom selection", "fg:ansibrightred"),
|
143
143
|
("ipywidget text text-area", "fg:black bg:white"),
|
144
|
+
("ipywidget text text-area disabled", "fg:#888888"),
|
144
145
|
("ipywidget text text-area focused", "fg:black bg:white"),
|
145
146
|
("ipywidget text placeholder", "fg:#AAAAAA bg:white"),
|
146
147
|
("ipywidget text border right", "fg:#E9E7E3"),
|
@@ -436,7 +437,7 @@ def build_style(
|
|
436
437
|
# Logo
|
437
438
|
"logo": "fg:#dd0000",
|
438
439
|
# Pattern
|
439
|
-
"pattern": f"fg:{cp.bg.more(0.
|
440
|
+
"pattern": f"fg:{cp.bg.more(0.05)}",
|
440
441
|
# Chrome
|
441
442
|
"chrome": f"fg:{cp.fg.more(0.05)} bg:{cp.bg.more(0.05)}",
|
442
443
|
"tab-padding": f"fg:{cp.bg.more(0.2)} bg:{cp.bg.base}",
|
euporie/core/suggest.py
CHANGED
@@ -2,13 +2,15 @@
|
|
2
2
|
|
3
3
|
from __future__ import annotations
|
4
4
|
|
5
|
+
import asyncio
|
5
6
|
import logging
|
6
|
-
from collections import defaultdict
|
7
|
+
from collections import defaultdict, deque
|
7
8
|
from difflib import SequenceMatcher
|
8
|
-
from functools import lru_cache
|
9
|
-
from typing import TYPE_CHECKING
|
9
|
+
from functools import lru_cache, partial
|
10
|
+
from typing import TYPE_CHECKING, NamedTuple
|
10
11
|
|
11
12
|
from prompt_toolkit.auto_suggest import AutoSuggest, ConditionalAutoSuggest, Suggestion
|
13
|
+
from prompt_toolkit.cache import SimpleCache
|
12
14
|
from prompt_toolkit.filters import to_filter
|
13
15
|
|
14
16
|
if TYPE_CHECKING:
|
@@ -21,39 +23,86 @@ if TYPE_CHECKING:
|
|
21
23
|
log = logging.getLogger(__name__)
|
22
24
|
|
23
25
|
|
24
|
-
class
|
26
|
+
class HistoryPosition(NamedTuple):
|
27
|
+
"""Store position information for a history match."""
|
28
|
+
|
29
|
+
idx: int # Index in history
|
30
|
+
context_start: int # Position where context starts
|
31
|
+
context_end: int # Position where context ends
|
32
|
+
|
33
|
+
|
34
|
+
class SmartHistoryAutoSuggest(AutoSuggest):
|
25
35
|
"""Suggest line completions from a :class:`History` object."""
|
26
36
|
|
37
|
+
_context_lines = 10
|
38
|
+
_max_line_len = 200
|
39
|
+
_max_item_lines = 1000
|
40
|
+
|
27
41
|
def __init__(self, history: History) -> None:
|
28
42
|
"""Set the kernel instance in initialization."""
|
29
43
|
self.history = history
|
30
|
-
self.calculate_similarity = lru_cache(maxsize=1024)(self._calculate_similarity)
|
31
44
|
|
32
45
|
self.n_texts = 0
|
33
|
-
self.
|
34
|
-
|
35
|
-
|
46
|
+
self._processing_task: asyncio.Task | None = None
|
47
|
+
# Index storage
|
48
|
+
self.prefix_tree: dict[str, list[int]] = defaultdict(list)
|
49
|
+
self.suffix_data: list[tuple[str, HistoryPosition]] = []
|
50
|
+
# Caches
|
51
|
+
self.calculate_similarity = lru_cache(maxsize=128)(self._calculate_similarity)
|
52
|
+
self.match_cache: SimpleCache[tuple[str, int, int], Suggestion | None] = (
|
53
|
+
SimpleCache(maxsize=128)
|
36
54
|
)
|
37
55
|
|
38
56
|
def process_history(self) -> None:
|
57
|
+
"""Schedule history processing if not already running."""
|
58
|
+
from euporie.notebook.current import get_app
|
59
|
+
|
60
|
+
if self._processing_task is not None and not self._processing_task.done():
|
61
|
+
return
|
62
|
+
|
63
|
+
# Schedule the actual processing to run when idle
|
64
|
+
self._processing_task = get_app().create_background_task(
|
65
|
+
self._process_history_async()
|
66
|
+
)
|
67
|
+
|
68
|
+
async def _process_history_async(self) -> None:
|
39
69
|
"""Process the entire history and store in prefix_dict."""
|
40
70
|
texts = self.history._loaded_strings
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
71
|
+
# Only process new history items
|
72
|
+
if not (texts := texts[: -self.n_texts or None]):
|
73
|
+
return
|
74
|
+
|
75
|
+
prefix_tree = self.prefix_tree
|
76
|
+
suffix_data = self.suffix_data
|
77
|
+
context_lines = self._context_lines
|
78
|
+
max_item_lines = self._max_item_lines
|
79
|
+
max_line_len = self._max_line_len
|
80
|
+
|
81
|
+
for i, text in enumerate(texts):
|
82
|
+
# Add tiny sleep to prevent blocking
|
83
|
+
if i > 1:
|
84
|
+
await asyncio.sleep(0.001)
|
85
|
+
|
86
|
+
lines = text.splitlines(keepends=True)
|
87
|
+
# Calculate positions of newlines
|
88
|
+
line_pos = [0]
|
89
|
+
for line in lines:
|
90
|
+
line_pos.append(line_pos[-1] + len(line))
|
91
|
+
# Index each line
|
92
|
+
for j, line in enumerate(lines[:max_item_lines]):
|
93
|
+
context_start = line_pos[max(0, j - context_lines)]
|
94
|
+
context_end = line_pos[min(j + context_lines, len(lines))]
|
95
|
+
hist_pos = HistoryPosition(
|
96
|
+
idx=i, context_start=context_start, context_end=context_end
|
97
|
+
)
|
98
|
+
line = line.strip()
|
99
|
+
# Create prefix/suffix combinations
|
100
|
+
for k in range(min(len(line), max_line_len)):
|
101
|
+
prefix, suffix = line[:k], line[k:]
|
102
|
+
prefix_tree[prefix].append(len(suffix_data))
|
103
|
+
suffix_data.append((suffix, hist_pos))
|
104
|
+
|
105
|
+
self.n_texts += len(texts)
|
57
106
|
|
58
107
|
def _calculate_similarity(self, text_1: str, text_2: str) -> float:
|
59
108
|
"""Calculate and cache the similarity between two texts."""
|
@@ -61,79 +110,111 @@ class HistoryAutoSuggest(AutoSuggest):
|
|
61
110
|
|
62
111
|
def get_suggestion(self, buffer: Buffer, document: Document) -> Suggestion | None:
|
63
112
|
"""Get a line completion suggestion."""
|
64
|
-
|
113
|
+
# Only return suggestions if cursor is at end of line
|
114
|
+
if not document.is_cursor_at_the_end_of_line:
|
115
|
+
return None
|
65
116
|
|
66
117
|
line = document.current_line.lstrip()
|
67
|
-
|
118
|
+
|
119
|
+
# Skip empty and very long lines
|
120
|
+
if not line or len(line) > self._max_line_len:
|
68
121
|
return None
|
69
122
|
|
70
|
-
|
123
|
+
# Schedule indexing any new history items
|
124
|
+
self.process_history()
|
71
125
|
|
72
|
-
|
73
|
-
|
126
|
+
# Find matches
|
127
|
+
key = line, hash(document.text), len(self.suffix_data)
|
128
|
+
return self.match_cache.get(key, partial(self._find_match, line, document.text))
|
74
129
|
|
130
|
+
def _find_match(self, line: str, document_text: str) -> Suggestion | None:
|
131
|
+
if not (suffix_indices := self.prefix_tree[line]):
|
132
|
+
return None
|
133
|
+
|
134
|
+
texts = self.history._loaded_strings
|
75
135
|
best_score = 0.0
|
76
|
-
best_suffix =
|
136
|
+
best_suffix = None
|
77
137
|
|
78
138
|
# Rank candidates
|
79
|
-
max_count = max(
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
139
|
+
max_count = max(1, len(suffix_indices))
|
140
|
+
suffix_groups: dict[str, list[HistoryPosition]] = defaultdict(list)
|
141
|
+
|
142
|
+
# Group suffixes and their positions
|
143
|
+
for idx in suffix_indices:
|
144
|
+
suffix, pos = self.suffix_data[idx]
|
145
|
+
suffix_groups[suffix].append(pos)
|
146
|
+
|
147
|
+
# Evaluate each unique suffix
|
148
|
+
for suffix, positions in suffix_groups.items():
|
149
|
+
count = len(positions)
|
150
|
+
for pos in positions[:10]:
|
151
|
+
# Get the text using the stored positions
|
152
|
+
text = texts[pos.idx]
|
153
|
+
context = text[pos.context_start : pos.context_end]
|
154
|
+
context_similarity = self.calculate_similarity(document_text, context)
|
85
155
|
score = (
|
86
|
-
|
87
|
-
|
88
|
-
# 0.333 * len(line) / len(match.group("prefix"))
|
89
|
-
# NUmber of instances in history
|
90
|
-
+ 0.3 * count / max_count
|
156
|
+
# Number of instances in history
|
157
|
+
0.3 * count / max_count
|
91
158
|
# Recentness
|
92
|
-
+ 0.3 *
|
159
|
+
+ 0.3 * pos.idx / len(texts)
|
93
160
|
# Similarity of context to document
|
94
161
|
+ 0.4 * context_similarity
|
95
162
|
)
|
96
|
-
|
97
|
-
if score > 0.
|
163
|
+
|
164
|
+
if score > 0.9:
|
98
165
|
return Suggestion(suffix)
|
99
166
|
if score > best_score:
|
100
167
|
best_score = score
|
101
168
|
best_suffix = suffix
|
169
|
+
|
102
170
|
if best_suffix:
|
103
171
|
return Suggestion(best_suffix)
|
104
172
|
return None
|
105
173
|
|
106
174
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
175
|
+
class SimpleHistoryAutoSuggest(AutoSuggest):
|
176
|
+
"""Suggest line completions from a :class:`History` object."""
|
177
|
+
|
178
|
+
def __init__(self, history: History, cache_size: int = 100_000) -> None:
|
179
|
+
"""Set the kernel instance in initialization."""
|
180
|
+
self.history = history
|
181
|
+
|
182
|
+
self.cache_size = cache_size
|
183
|
+
self.cache_keys: deque[str] = deque()
|
184
|
+
self.cache: dict[str, Suggestion] = {}
|
185
|
+
|
186
|
+
def get_suggestion(self, buffer: Buffer, document: Document) -> Suggestion | None:
|
187
|
+
"""Get a line completion suggestion."""
|
188
|
+
result: Suggestion | None = None
|
189
|
+
line = document.current_line.lstrip()
|
190
|
+
if line:
|
191
|
+
if line in self.cache:
|
192
|
+
result = self.cache[line]
|
193
|
+
else:
|
194
|
+
result = self.lookup_suggestion(line)
|
195
|
+
if result:
|
196
|
+
if len(self.cache) > self.cache_size:
|
197
|
+
key_to_remove = self.cache_keys.popleft()
|
198
|
+
if key_to_remove in self.cache:
|
199
|
+
del self.cache[key_to_remove]
|
200
|
+
|
201
|
+
self.cache_keys.append(line)
|
202
|
+
self.cache[line] = result
|
203
|
+
return result
|
204
|
+
|
205
|
+
def lookup_suggestion(self, line: str) -> Suggestion | None:
|
206
|
+
"""Find the most recent matching line in the history."""
|
207
|
+
# Loop history, most recent item first
|
208
|
+
for text in self.history._loaded_strings:
|
209
|
+
if line in text:
|
210
|
+
# Loop over lines of item in reverse order
|
211
|
+
for hist_line in text.splitlines()[::-1]:
|
212
|
+
hist_line = hist_line.strip()
|
213
|
+
if hist_line.startswith(line):
|
214
|
+
# Return from the match to end from the history line
|
215
|
+
suggestion = hist_line[len(line) :]
|
216
|
+
return Suggestion(suggestion)
|
217
|
+
return None
|
137
218
|
|
138
219
|
|
139
220
|
class ConditionalAutoSuggestAsync(ConditionalAutoSuggest):
|
euporie/core/tabs/__init__.py
CHANGED
@@ -29,5 +29,15 @@ class TabRegistryEntry:
|
|
29
29
|
"""Sort by weight."""
|
30
30
|
return self.weight < other.weight
|
31
31
|
|
32
|
+
def __hash__(self) -> int:
|
33
|
+
"""Make the class hashable based on its path."""
|
34
|
+
return hash(self.path)
|
35
|
+
|
36
|
+
def __eq__(self, other: object) -> bool:
|
37
|
+
"""Compare TabRegistryEntry objects based on their path."""
|
38
|
+
if not isinstance(other, TabRegistryEntry):
|
39
|
+
return NotImplemented
|
40
|
+
return self.path == other.path
|
41
|
+
|
32
42
|
|
33
43
|
_TAB_REGISTRY: list[TabRegistryEntry] = []
|