euporie 2.8.6__py3-none-any.whl → 2.8.8__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.
Files changed (65) hide show
  1. euporie/console/app.py +2 -0
  2. euporie/console/tabs/console.py +27 -17
  3. euporie/core/__init__.py +2 -2
  4. euporie/core/app/_commands.py +4 -21
  5. euporie/core/app/app.py +13 -7
  6. euporie/core/bars/command.py +9 -6
  7. euporie/core/bars/search.py +43 -2
  8. euporie/core/border.py +7 -2
  9. euporie/core/comm/base.py +2 -2
  10. euporie/core/comm/ipywidgets.py +3 -3
  11. euporie/core/commands.py +44 -8
  12. euporie/core/completion.py +14 -6
  13. euporie/core/convert/datum.py +7 -7
  14. euporie/core/data_structures.py +20 -1
  15. euporie/core/filters.py +8 -0
  16. euporie/core/ft/html.py +47 -40
  17. euporie/core/graphics.py +11 -3
  18. euporie/core/history.py +15 -5
  19. euporie/core/inspection.py +16 -9
  20. euporie/core/io.py +1 -1
  21. euporie/core/kernel/__init__.py +53 -1
  22. euporie/core/kernel/base.py +571 -0
  23. euporie/core/kernel/{client.py → jupyter.py} +173 -430
  24. euporie/core/kernel/{manager.py → jupyter_manager.py} +4 -3
  25. euporie/core/kernel/local.py +694 -0
  26. euporie/core/key_binding/bindings/basic.py +6 -3
  27. euporie/core/keys.py +26 -25
  28. euporie/core/layout/cache.py +31 -7
  29. euporie/core/layout/containers.py +88 -13
  30. euporie/core/layout/scroll.py +45 -148
  31. euporie/core/log.py +1 -1
  32. euporie/core/style.py +2 -1
  33. euporie/core/suggest.py +155 -74
  34. euporie/core/tabs/__init__.py +10 -0
  35. euporie/core/tabs/_commands.py +76 -0
  36. euporie/core/tabs/_settings.py +16 -0
  37. euporie/core/tabs/base.py +22 -8
  38. euporie/core/tabs/kernel.py +81 -35
  39. euporie/core/tabs/notebook.py +14 -22
  40. euporie/core/utils.py +1 -1
  41. euporie/core/validation.py +8 -8
  42. euporie/core/widgets/_settings.py +19 -2
  43. euporie/core/widgets/cell.py +31 -31
  44. euporie/core/widgets/cell_outputs.py +10 -1
  45. euporie/core/widgets/dialog.py +30 -75
  46. euporie/core/widgets/forms.py +71 -59
  47. euporie/core/widgets/inputs.py +7 -4
  48. euporie/core/widgets/layout.py +281 -93
  49. euporie/core/widgets/menu.py +55 -15
  50. euporie/core/widgets/palette.py +3 -1
  51. euporie/core/widgets/tree.py +86 -76
  52. euporie/notebook/app.py +35 -16
  53. euporie/notebook/tabs/edit.py +4 -4
  54. euporie/notebook/tabs/json.py +6 -2
  55. euporie/notebook/tabs/notebook.py +26 -8
  56. euporie/preview/tabs/notebook.py +17 -13
  57. euporie/web/tabs/web.py +22 -3
  58. euporie/web/widgets/webview.py +3 -0
  59. {euporie-2.8.6.dist-info → euporie-2.8.8.dist-info}/METADATA +1 -1
  60. {euporie-2.8.6.dist-info → euporie-2.8.8.dist-info}/RECORD +65 -62
  61. {euporie-2.8.6.dist-info → euporie-2.8.8.dist-info}/entry_points.txt +1 -1
  62. {euporie-2.8.6.dist-info → euporie-2.8.8.dist-info}/licenses/LICENSE +1 -1
  63. {euporie-2.8.6.data → euporie-2.8.8.data}/data/share/applications/euporie-console.desktop +0 -0
  64. {euporie-2.8.6.data → euporie-2.8.8.data}/data/share/applications/euporie-notebook.desktop +0 -0
  65. {euporie-2.8.6.dist-info → euporie-2.8.8.dist-info}/WHEEL +0 -0
@@ -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
- (bottom_pos + bottom_child.height + self.scrolling)
268
- - self.last_write_position.height,
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
- if self.scrolling:
419
- heights = [
420
- # Ensure unrendered cells have at least some height
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(self.known_sizes.values()), 1)),
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(list(self.known_sizes.values())[: self._selected_slice.start])
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(list(self.known_sizes.values())[: self._selected_slice.start]) - value
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 decorator which captures standard output and logs it."""
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.075)}",
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 HistoryAutoSuggest(AutoSuggest):
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.n_lines = 0
34
- self.prefix_dict: dict[str, dict[str, list[dict[str, int]]]] = defaultdict(
35
- lambda: defaultdict(list)
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
- if texts := texts[: len(texts) - self.n_texts]:
42
- n_lines = self.n_lines
43
- prefix_dict = self.prefix_dict
44
- for i, text in enumerate(reversed(texts)):
45
- for line in text.strip().splitlines():
46
- n_lines += 1
47
- line = line.strip()
48
- for j in range(1, len(line)):
49
- prefix, suffix = line[:j], line[j:]
50
- prefix_dict[prefix][suffix].append(
51
- {"index": -1 - i, "line": n_lines}
52
- )
53
- # for k in range(1, len(prefix)):
54
- # prefix_dict[prefix[-k:]] = prefix_dict[prefix]
55
- self.n_lines = n_lines
56
- self.n_texts += len(texts)
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
- self.process_history()
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
- if not line:
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
- suffixes = self.prefix_dict[line]
123
+ # Schedule indexing any new history items
124
+ self.process_history()
71
125
 
72
- texts = self.history._loaded_strings
73
- n_lines = self.n_lines
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([1, *(len(x) for x in suffixes.values())])
80
- for suffix, instances in suffixes.items():
81
- count = len(instances)
82
- for instance in instances:
83
- text = texts[instance["index"]]
84
- context_similarity = self.calculate_similarity(document.text, text)
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
- 0
87
- # Similarity of prefix to line
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 * instance["line"] / n_lines
159
+ + 0.3 * pos.idx / len(texts)
93
160
  # Similarity of context to document
94
161
  + 0.4 * context_similarity
95
162
  )
96
- # log.debug("%s %r", score, suffix)
97
- if score > 0.95:
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
- # class KernelAutoSuggest(AutoSuggest):
108
- # """Suggest line completions from kernel history."""
109
-
110
- # def __init__(self, kernel: Kernel) -> None:
111
- # """Set the kernel instance in initialization."""
112
- # self.kernel = kernel
113
-
114
- # def get_suggestion(self, buffer: Buffer, document: Document) -> Suggestion | None:
115
- # """Doe nothing."""
116
- # return None
117
-
118
- # async def get_suggestion_async(
119
- # self, buff: Buffer, document: Document
120
- # ) -> Suggestion | None:
121
- # """Return suggestions based on matching kernel history."""
122
- # line = document.current_line.strip()
123
- # if line:
124
- # suggestions = await self.kernel.history_(f"*{line}*")
125
- # log.debug("Suggestor got suggestions %s", suggestions)
126
- # if suggestions:
127
- # _, _, text = suggestions[0]
128
- # # Find matching line
129
- # for hist_line in text.split("\n"):
130
- # hist_line = hist_line.strip()
131
- # if hist_line.startswith(line):
132
- # # Return from the match to end from the history line
133
- # suggestion = hist_line[len(line) :]
134
- # log.debug("Suggesting %s", suggestion)
135
- # return Suggestion(suggestion)
136
- # return None
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):
@@ -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] = []