sqlshell 0.4.4__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.
- sqlshell/__init__.py +84 -0
- sqlshell/__main__.py +4926 -0
- sqlshell/ai_autocomplete.py +392 -0
- sqlshell/ai_settings_dialog.py +337 -0
- sqlshell/context_suggester.py +768 -0
- sqlshell/create_test_data.py +152 -0
- sqlshell/data/create_test_data.py +137 -0
- sqlshell/db/__init__.py +6 -0
- sqlshell/db/database_manager.py +1318 -0
- sqlshell/db/export_manager.py +188 -0
- sqlshell/editor.py +1166 -0
- sqlshell/editor_integration.py +127 -0
- sqlshell/execution_handler.py +421 -0
- sqlshell/menus.py +262 -0
- sqlshell/notification_manager.py +370 -0
- sqlshell/query_tab.py +904 -0
- sqlshell/resources/__init__.py +1 -0
- sqlshell/resources/icon.png +0 -0
- sqlshell/resources/logo_large.png +0 -0
- sqlshell/resources/logo_medium.png +0 -0
- sqlshell/resources/logo_small.png +0 -0
- sqlshell/resources/splash_screen.gif +0 -0
- sqlshell/space_invaders.py +501 -0
- sqlshell/splash_screen.py +405 -0
- sqlshell/sqlshell/__init__.py +5 -0
- sqlshell/sqlshell/create_test_data.py +118 -0
- sqlshell/sqlshell/create_test_databases.py +96 -0
- sqlshell/sqlshell_demo.png +0 -0
- sqlshell/styles.py +257 -0
- sqlshell/suggester_integration.py +330 -0
- sqlshell/syntax_highlighter.py +124 -0
- sqlshell/table_list.py +996 -0
- sqlshell/ui/__init__.py +6 -0
- sqlshell/ui/bar_chart_delegate.py +49 -0
- sqlshell/ui/filter_header.py +469 -0
- sqlshell/utils/__init__.py +16 -0
- sqlshell/utils/profile_cn2.py +1661 -0
- sqlshell/utils/profile_column.py +2635 -0
- sqlshell/utils/profile_distributions.py +616 -0
- sqlshell/utils/profile_entropy.py +347 -0
- sqlshell/utils/profile_foreign_keys.py +779 -0
- sqlshell/utils/profile_keys.py +2834 -0
- sqlshell/utils/profile_ohe.py +934 -0
- sqlshell/utils/profile_ohe_advanced.py +754 -0
- sqlshell/utils/profile_ohe_comparison.py +237 -0
- sqlshell/utils/profile_prediction.py +926 -0
- sqlshell/utils/profile_similarity.py +876 -0
- sqlshell/utils/search_in_df.py +90 -0
- sqlshell/widgets.py +400 -0
- sqlshell-0.4.4.dist-info/METADATA +441 -0
- sqlshell-0.4.4.dist-info/RECORD +54 -0
- sqlshell-0.4.4.dist-info/WHEEL +5 -0
- sqlshell-0.4.4.dist-info/entry_points.txt +2 -0
- sqlshell-0.4.4.dist-info/top_level.txt +1 -0
sqlshell/styles.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
def get_application_stylesheet(colors):
|
|
2
|
+
"""Generate the application's stylesheet using the provided color scheme.
|
|
3
|
+
|
|
4
|
+
Args:
|
|
5
|
+
colors: A dictionary containing color definitions for the application
|
|
6
|
+
|
|
7
|
+
Returns:
|
|
8
|
+
A string containing the complete Qt stylesheet
|
|
9
|
+
"""
|
|
10
|
+
return f"""
|
|
11
|
+
QMainWindow {{
|
|
12
|
+
background-color: {colors['background']};
|
|
13
|
+
}}
|
|
14
|
+
|
|
15
|
+
QWidget {{
|
|
16
|
+
color: {colors['text']};
|
|
17
|
+
font-family: 'Segoe UI', 'Arial', sans-serif;
|
|
18
|
+
}}
|
|
19
|
+
|
|
20
|
+
QLabel {{
|
|
21
|
+
font-size: 13px;
|
|
22
|
+
padding: 2px;
|
|
23
|
+
}}
|
|
24
|
+
|
|
25
|
+
QLabel#header_label {{
|
|
26
|
+
font-size: 16px;
|
|
27
|
+
font-weight: bold;
|
|
28
|
+
color: {colors['primary']};
|
|
29
|
+
padding: 8px 0;
|
|
30
|
+
}}
|
|
31
|
+
|
|
32
|
+
QPushButton {{
|
|
33
|
+
background-color: {colors['secondary']};
|
|
34
|
+
color: white;
|
|
35
|
+
border: none;
|
|
36
|
+
border-radius: 4px;
|
|
37
|
+
padding: 8px 16px;
|
|
38
|
+
font-weight: bold;
|
|
39
|
+
font-size: 13px;
|
|
40
|
+
min-height: 30px;
|
|
41
|
+
}}
|
|
42
|
+
|
|
43
|
+
QPushButton:hover {{
|
|
44
|
+
background-color: #2980B9;
|
|
45
|
+
}}
|
|
46
|
+
|
|
47
|
+
QPushButton:pressed {{
|
|
48
|
+
background-color: #1F618D;
|
|
49
|
+
}}
|
|
50
|
+
|
|
51
|
+
QPushButton#primary_button {{
|
|
52
|
+
background-color: {colors['accent']};
|
|
53
|
+
}}
|
|
54
|
+
|
|
55
|
+
QPushButton#primary_button:hover {{
|
|
56
|
+
background-color: #16A085;
|
|
57
|
+
}}
|
|
58
|
+
|
|
59
|
+
QPushButton#primary_button:pressed {{
|
|
60
|
+
background-color: #0E6655;
|
|
61
|
+
}}
|
|
62
|
+
|
|
63
|
+
QPushButton#danger_button {{
|
|
64
|
+
background-color: {colors['error']};
|
|
65
|
+
}}
|
|
66
|
+
|
|
67
|
+
QPushButton#danger_button:hover {{
|
|
68
|
+
background-color: #CB4335;
|
|
69
|
+
}}
|
|
70
|
+
|
|
71
|
+
QToolButton {{
|
|
72
|
+
background-color: transparent;
|
|
73
|
+
border: none;
|
|
74
|
+
border-radius: 4px;
|
|
75
|
+
padding: 4px;
|
|
76
|
+
}}
|
|
77
|
+
|
|
78
|
+
QToolButton:hover {{
|
|
79
|
+
background-color: rgba(52, 152, 219, 0.2);
|
|
80
|
+
}}
|
|
81
|
+
|
|
82
|
+
QFrame#sidebar {{
|
|
83
|
+
background-color: {colors['primary']};
|
|
84
|
+
border-radius: 0px;
|
|
85
|
+
}}
|
|
86
|
+
|
|
87
|
+
QFrame#content_panel {{
|
|
88
|
+
background-color: white;
|
|
89
|
+
border-radius: 8px;
|
|
90
|
+
border: 1px solid {colors['border']};
|
|
91
|
+
}}
|
|
92
|
+
|
|
93
|
+
QListWidget {{
|
|
94
|
+
background-color: white;
|
|
95
|
+
border-radius: 4px;
|
|
96
|
+
border: 1px solid {colors['border']};
|
|
97
|
+
padding: 4px;
|
|
98
|
+
outline: none;
|
|
99
|
+
}}
|
|
100
|
+
|
|
101
|
+
QListWidget::item {{
|
|
102
|
+
padding: 8px;
|
|
103
|
+
border-radius: 4px;
|
|
104
|
+
}}
|
|
105
|
+
|
|
106
|
+
QListWidget::item:selected {{
|
|
107
|
+
background-color: {colors['secondary']};
|
|
108
|
+
color: white;
|
|
109
|
+
}}
|
|
110
|
+
|
|
111
|
+
QListWidget::item:hover:!selected {{
|
|
112
|
+
background-color: #E3F2FD;
|
|
113
|
+
}}
|
|
114
|
+
|
|
115
|
+
QTableWidget {{
|
|
116
|
+
background-color: white;
|
|
117
|
+
alternate-background-color: #F8F9FA;
|
|
118
|
+
border-radius: 4px;
|
|
119
|
+
border: 1px solid {colors['border']};
|
|
120
|
+
gridline-color: #E0E0E0;
|
|
121
|
+
outline: none;
|
|
122
|
+
}}
|
|
123
|
+
|
|
124
|
+
QTableWidget::item {{
|
|
125
|
+
padding: 4px;
|
|
126
|
+
}}
|
|
127
|
+
|
|
128
|
+
QTableWidget::item:selected {{
|
|
129
|
+
background-color: rgba(52, 152, 219, 0.2);
|
|
130
|
+
color: {colors['text']};
|
|
131
|
+
}}
|
|
132
|
+
|
|
133
|
+
QHeaderView::section {{
|
|
134
|
+
background-color: {colors['primary']};
|
|
135
|
+
color: white;
|
|
136
|
+
padding: 8px;
|
|
137
|
+
border: none;
|
|
138
|
+
font-weight: bold;
|
|
139
|
+
}}
|
|
140
|
+
|
|
141
|
+
QSplitter::handle {{
|
|
142
|
+
background-color: {colors['border']};
|
|
143
|
+
}}
|
|
144
|
+
|
|
145
|
+
QStatusBar {{
|
|
146
|
+
background-color: {colors['primary']};
|
|
147
|
+
color: white;
|
|
148
|
+
padding: 8px;
|
|
149
|
+
}}
|
|
150
|
+
|
|
151
|
+
QTabWidget::pane {{
|
|
152
|
+
border: 1px solid {colors['border']};
|
|
153
|
+
border-radius: 4px;
|
|
154
|
+
top: -1px;
|
|
155
|
+
background-color: white;
|
|
156
|
+
}}
|
|
157
|
+
|
|
158
|
+
QTabBar::tab {{
|
|
159
|
+
background-color: {colors['light_bg']};
|
|
160
|
+
color: {colors['text']};
|
|
161
|
+
border: 1px solid {colors['border']};
|
|
162
|
+
border-bottom: none;
|
|
163
|
+
border-top-left-radius: 4px;
|
|
164
|
+
border-top-right-radius: 4px;
|
|
165
|
+
padding: 8px 12px;
|
|
166
|
+
margin-right: 2px;
|
|
167
|
+
min-width: 80px;
|
|
168
|
+
}}
|
|
169
|
+
|
|
170
|
+
QTabBar::tab:selected {{
|
|
171
|
+
background-color: white;
|
|
172
|
+
border-bottom: 1px solid white;
|
|
173
|
+
}}
|
|
174
|
+
|
|
175
|
+
QTabBar::tab:hover:!selected {{
|
|
176
|
+
background-color: #E3F2FD;
|
|
177
|
+
}}
|
|
178
|
+
|
|
179
|
+
QTabBar::close-button {{
|
|
180
|
+
image: url(close.png);
|
|
181
|
+
subcontrol-position: right;
|
|
182
|
+
}}
|
|
183
|
+
|
|
184
|
+
QTabBar::close-button:hover {{
|
|
185
|
+
background-color: rgba(255, 0, 0, 0.2);
|
|
186
|
+
border-radius: 2px;
|
|
187
|
+
}}
|
|
188
|
+
|
|
189
|
+
QPlainTextEdit, QTextEdit {{
|
|
190
|
+
background-color: white;
|
|
191
|
+
border-radius: 4px;
|
|
192
|
+
border: 1px solid {colors['border']};
|
|
193
|
+
padding: 8px;
|
|
194
|
+
selection-background-color: #BBDEFB;
|
|
195
|
+
selection-color: {colors['text']};
|
|
196
|
+
font-family: 'Consolas', 'Courier New', monospace;
|
|
197
|
+
font-size: 14px;
|
|
198
|
+
}}
|
|
199
|
+
"""
|
|
200
|
+
|
|
201
|
+
def get_tab_corner_stylesheet():
|
|
202
|
+
"""Get the stylesheet for the tab corner widget with the + button"""
|
|
203
|
+
return """
|
|
204
|
+
QToolButton {
|
|
205
|
+
background-color: transparent;
|
|
206
|
+
border: none;
|
|
207
|
+
border-radius: 4px;
|
|
208
|
+
padding: 4px;
|
|
209
|
+
font-weight: bold;
|
|
210
|
+
font-size: 16px;
|
|
211
|
+
color: #3498DB;
|
|
212
|
+
}
|
|
213
|
+
QToolButton:hover {
|
|
214
|
+
background-color: rgba(52, 152, 219, 0.2);
|
|
215
|
+
}
|
|
216
|
+
QToolButton:pressed {
|
|
217
|
+
background-color: rgba(52, 152, 219, 0.4);
|
|
218
|
+
}
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
def get_context_menu_stylesheet():
|
|
222
|
+
"""Get the stylesheet for context menus"""
|
|
223
|
+
return """
|
|
224
|
+
QMenu {
|
|
225
|
+
background-color: white;
|
|
226
|
+
border: 1px solid #BDC3C7;
|
|
227
|
+
padding: 5px;
|
|
228
|
+
}
|
|
229
|
+
QMenu::item {
|
|
230
|
+
padding: 5px 20px;
|
|
231
|
+
}
|
|
232
|
+
QMenu::item:selected {
|
|
233
|
+
background-color: #3498DB;
|
|
234
|
+
color: white;
|
|
235
|
+
}
|
|
236
|
+
QMenu::separator {
|
|
237
|
+
height: 1px;
|
|
238
|
+
background-color: #BDC3C7;
|
|
239
|
+
margin: 5px 15px;
|
|
240
|
+
}
|
|
241
|
+
"""
|
|
242
|
+
|
|
243
|
+
def get_header_label_stylesheet():
|
|
244
|
+
"""Get the stylesheet for header labels"""
|
|
245
|
+
return "color: white; font-weight: bold; font-size: 14px;"
|
|
246
|
+
|
|
247
|
+
def get_db_info_label_stylesheet():
|
|
248
|
+
"""Get the stylesheet for database info label"""
|
|
249
|
+
return "color: rgba(255, 255, 255, 0.8); padding: 8px 0; font-size: 13px;"
|
|
250
|
+
|
|
251
|
+
def get_tables_header_stylesheet():
|
|
252
|
+
"""Get the stylesheet for tables header"""
|
|
253
|
+
return "color: white; font-weight: bold; font-size: 14px; margin-top: 8px;"
|
|
254
|
+
|
|
255
|
+
def get_row_count_label_stylesheet():
|
|
256
|
+
"""Get the stylesheet for row count label"""
|
|
257
|
+
return "color: #7F8C8D; font-size: 12px; font-style: italic; padding: 8px 0;"
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Integration module for context-aware SQL suggestions.
|
|
3
|
+
|
|
4
|
+
This module provides the glue code needed to connect the ContextSuggester
|
|
5
|
+
with the SQL editor component for seamless context-aware autocompletion.
|
|
6
|
+
Also integrates AI-powered suggestions when configured.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from PyQt6.QtCore import QStringListModel, Qt, QMetaObject, Q_ARG
|
|
10
|
+
from PyQt6.QtWidgets import QCompleter
|
|
11
|
+
from typing import Dict, List, Any, Optional
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
from sqlshell.context_suggester import ContextSuggester
|
|
15
|
+
from sqlshell.ai_autocomplete import get_ai_autocomplete_manager
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class SuggestionManager:
|
|
19
|
+
"""
|
|
20
|
+
Manages the integration between the ContextSuggester and the SQLEditor.
|
|
21
|
+
|
|
22
|
+
This class acts as a bridge between the database schema information,
|
|
23
|
+
query history tracking, and the editor's autocompletion functionality.
|
|
24
|
+
Also integrates AI-powered suggestions when configured.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
"""Initialize the suggestion manager."""
|
|
29
|
+
self.suggester = ContextSuggester()
|
|
30
|
+
self._completers = {} # {editor_id: completer}
|
|
31
|
+
self._editors = {} # {editor_id: editor_instance}
|
|
32
|
+
self._ai_manager = get_ai_autocomplete_manager()
|
|
33
|
+
|
|
34
|
+
# Connect AI suggestion signal to handler with QueuedConnection for thread safety
|
|
35
|
+
# This ensures signals from background threads are processed in the main thread
|
|
36
|
+
self._ai_manager.suggestion_ready.connect(
|
|
37
|
+
self._on_ai_suggestion_ready,
|
|
38
|
+
Qt.ConnectionType.QueuedConnection
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# Track which editor requested AI suggestion
|
|
42
|
+
self._pending_ai_editor_id = None
|
|
43
|
+
|
|
44
|
+
def register_editor(self, editor, editor_id=None):
|
|
45
|
+
"""
|
|
46
|
+
Register an editor to receive context-aware suggestions.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
editor: The SQLEditor instance to register
|
|
50
|
+
editor_id: Optional identifier for the editor (defaults to object id)
|
|
51
|
+
"""
|
|
52
|
+
print(f"[AI DEBUG] register_editor called with editor_id={editor_id}")
|
|
53
|
+
if editor_id is None:
|
|
54
|
+
editor_id = id(editor)
|
|
55
|
+
|
|
56
|
+
# Create a completer for this editor if it doesn't have one
|
|
57
|
+
if not hasattr(editor, 'completer') or not editor.completer:
|
|
58
|
+
completer = QCompleter()
|
|
59
|
+
completer.setWidget(editor)
|
|
60
|
+
completer.setCompletionMode(QCompleter.CompletionMode.PopupCompletion)
|
|
61
|
+
completer.setCaseSensitivity(Qt.CaseSensitivity.CaseInsensitive)
|
|
62
|
+
completer.activated.connect(editor.insert_completion)
|
|
63
|
+
editor.completer = completer
|
|
64
|
+
|
|
65
|
+
self._completers[editor_id] = editor.completer
|
|
66
|
+
self._editors[editor_id] = editor
|
|
67
|
+
|
|
68
|
+
# Hook into editor's context detection methods if possible
|
|
69
|
+
if hasattr(editor, 'get_context_at_cursor'):
|
|
70
|
+
# Save the original method
|
|
71
|
+
if not hasattr(editor, '_original_get_context_at_cursor'):
|
|
72
|
+
editor._original_get_context_at_cursor = editor.get_context_at_cursor
|
|
73
|
+
|
|
74
|
+
# Replace with our enhanced version
|
|
75
|
+
def enhanced_get_context(editor_ref=editor, suggestion_mgr=self):
|
|
76
|
+
# Get the original context first
|
|
77
|
+
original_context = editor_ref._original_get_context_at_cursor()
|
|
78
|
+
|
|
79
|
+
# Get our enhanced context
|
|
80
|
+
tc = editor_ref.textCursor()
|
|
81
|
+
position = tc.position()
|
|
82
|
+
doc = editor_ref.document()
|
|
83
|
+
|
|
84
|
+
# Get text before cursor - the error was in this section
|
|
85
|
+
# Using a simpler approach that doesn't rely on QTextDocument.find()
|
|
86
|
+
text_before_cursor = editor_ref.toPlainText()[:position]
|
|
87
|
+
current_word = editor_ref.get_word_under_cursor()
|
|
88
|
+
|
|
89
|
+
enhanced_context = suggestion_mgr.suggester.analyze_context(
|
|
90
|
+
text_before_cursor,
|
|
91
|
+
current_word
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
# Merge the contexts (our enhanced context takes precedence)
|
|
95
|
+
merged_context = {**original_context, **enhanced_context}
|
|
96
|
+
return merged_context
|
|
97
|
+
|
|
98
|
+
editor.get_context_at_cursor = enhanced_get_context
|
|
99
|
+
|
|
100
|
+
# Hook into editor's complete method if possible
|
|
101
|
+
print(f"[AI DEBUG] Checking if editor has 'complete': {hasattr(editor, 'complete')}")
|
|
102
|
+
if hasattr(editor, 'complete'):
|
|
103
|
+
# Save the original method
|
|
104
|
+
print(f"[AI DEBUG] Has _original_complete already? {hasattr(editor, '_original_complete')}")
|
|
105
|
+
if not hasattr(editor, '_original_complete'):
|
|
106
|
+
editor._original_complete = editor.complete
|
|
107
|
+
print(f"[AI DEBUG] Saved _original_complete: {editor._original_complete}")
|
|
108
|
+
|
|
109
|
+
# Replace with our enhanced version for ghost text
|
|
110
|
+
def enhanced_complete(editor_ref=editor, suggestion_mgr=self):
|
|
111
|
+
print(f"[AI DEBUG] enhanced_complete called!")
|
|
112
|
+
tc = editor_ref.textCursor()
|
|
113
|
+
position = tc.position()
|
|
114
|
+
text_before_cursor = editor_ref.toPlainText()[:position]
|
|
115
|
+
current_word = editor_ref.get_word_under_cursor()
|
|
116
|
+
print(f"[AI DEBUG] position={position}, text_len={len(text_before_cursor)}, word='{current_word}'")
|
|
117
|
+
|
|
118
|
+
# Check if Ctrl key is being held down
|
|
119
|
+
from PyQt6.QtWidgets import QApplication
|
|
120
|
+
from PyQt6.QtCore import Qt
|
|
121
|
+
|
|
122
|
+
# Don't show completions if Ctrl key is pressed (could be in preparation for Ctrl+Enter)
|
|
123
|
+
modifiers = QApplication.keyboardModifiers()
|
|
124
|
+
if modifiers & Qt.KeyboardModifier.ControlModifier:
|
|
125
|
+
# Clear any ghost text if Ctrl is held down
|
|
126
|
+
if hasattr(editor_ref, 'clear_ghost_text'):
|
|
127
|
+
editor_ref.clear_ghost_text()
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
# Special handling for function argument completions
|
|
131
|
+
# This helps with context like SELECT AVG(...) FROM table
|
|
132
|
+
in_function = False
|
|
133
|
+
open_parens = text_before_cursor.count('(')
|
|
134
|
+
close_parens = text_before_cursor.count(')')
|
|
135
|
+
|
|
136
|
+
if open_parens > close_parens:
|
|
137
|
+
in_function = True
|
|
138
|
+
# Get further context for better suggestions inside function arguments
|
|
139
|
+
context = suggestion_mgr.suggester.analyze_context(text_before_cursor, current_word)
|
|
140
|
+
if 'tables_in_from' not in context or not context['tables_in_from']:
|
|
141
|
+
# If tables not yet detected, try to look ahead for FROM clause
|
|
142
|
+
full_text = editor_ref.toPlainText()
|
|
143
|
+
after_cursor = full_text[position:]
|
|
144
|
+
# Look for FROM clause after current position
|
|
145
|
+
from_match = re.search(r'FROM\s+([a-zA-Z0-9_]+)', after_cursor, re.IGNORECASE)
|
|
146
|
+
if from_match:
|
|
147
|
+
table_name = from_match.group(1)
|
|
148
|
+
# Add this table to the context for better suggestions
|
|
149
|
+
context['tables_in_from'] = [table_name]
|
|
150
|
+
# Update the context in suggester
|
|
151
|
+
suggestion_mgr.suggester._context_cache[f"{text_before_cursor}:{current_word}"] = context
|
|
152
|
+
|
|
153
|
+
# Get context-aware suggestions from the local suggester
|
|
154
|
+
suggestions = suggestion_mgr.get_suggestions(text_before_cursor, current_word)
|
|
155
|
+
|
|
156
|
+
# Check if AI autocomplete is available and should be used
|
|
157
|
+
ai_manager = suggestion_mgr._ai_manager
|
|
158
|
+
use_ai = (
|
|
159
|
+
ai_manager.is_available and
|
|
160
|
+
len(text_before_cursor.strip()) >= 3 # Only use AI for non-trivial context
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if suggestions and hasattr(editor_ref, 'show_ghost_text'):
|
|
164
|
+
# Find the best suggestion using the same logic as the editor's complete method
|
|
165
|
+
prefix = editor_ref.text_under_cursor()
|
|
166
|
+
|
|
167
|
+
# Sort by relevance - prioritize exact prefix matches and shorter suggestions
|
|
168
|
+
def relevance_score(item):
|
|
169
|
+
item_lower = item.lower()
|
|
170
|
+
prefix_lower = prefix.lower()
|
|
171
|
+
|
|
172
|
+
# Perfect case match gets highest priority
|
|
173
|
+
if item.startswith(prefix):
|
|
174
|
+
return (0, len(item))
|
|
175
|
+
# Case-insensitive prefix match
|
|
176
|
+
elif item_lower.startswith(prefix_lower):
|
|
177
|
+
return (1, len(item))
|
|
178
|
+
# Contains the prefix somewhere
|
|
179
|
+
elif prefix_lower in item_lower:
|
|
180
|
+
return (2, len(item))
|
|
181
|
+
else:
|
|
182
|
+
return (3, len(item))
|
|
183
|
+
|
|
184
|
+
suggestions.sort(key=relevance_score)
|
|
185
|
+
best_suggestion = suggestions[0]
|
|
186
|
+
|
|
187
|
+
# Show ghost text for the best suggestion
|
|
188
|
+
editor_ref.show_ghost_text(best_suggestion, position)
|
|
189
|
+
|
|
190
|
+
# Also request AI suggestion in background (may override if better)
|
|
191
|
+
if use_ai:
|
|
192
|
+
suggestion_mgr._pending_ai_editor_id = id(editor_ref)
|
|
193
|
+
ai_manager.request_suggestion(
|
|
194
|
+
text_before_cursor,
|
|
195
|
+
current_word,
|
|
196
|
+
position
|
|
197
|
+
)
|
|
198
|
+
elif use_ai:
|
|
199
|
+
# No local suggestions, try AI
|
|
200
|
+
suggestion_mgr._pending_ai_editor_id = id(editor_ref)
|
|
201
|
+
ai_manager.request_suggestion(
|
|
202
|
+
text_before_cursor,
|
|
203
|
+
current_word,
|
|
204
|
+
position
|
|
205
|
+
)
|
|
206
|
+
else:
|
|
207
|
+
# Clear ghost text if no suggestions
|
|
208
|
+
if hasattr(editor_ref, 'clear_ghost_text'):
|
|
209
|
+
editor_ref.clear_ghost_text()
|
|
210
|
+
|
|
211
|
+
# Fall back to original completion if no context-aware suggestions
|
|
212
|
+
if hasattr(editor_ref, '_original_complete'):
|
|
213
|
+
editor_ref._original_complete()
|
|
214
|
+
|
|
215
|
+
editor.complete = enhanced_complete
|
|
216
|
+
print(f"[AI DEBUG] Replaced complete method. Now: {editor.complete}")
|
|
217
|
+
|
|
218
|
+
def unregister_editor(self, editor_id):
|
|
219
|
+
"""
|
|
220
|
+
Unregister an editor from receiving context-aware suggestions.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
editor_id: The identifier of the editor to unregister
|
|
224
|
+
"""
|
|
225
|
+
if editor_id in self._editors:
|
|
226
|
+
editor = self._editors[editor_id]
|
|
227
|
+
|
|
228
|
+
# Restore original methods if we replaced them
|
|
229
|
+
if hasattr(editor, '_original_get_context_at_cursor'):
|
|
230
|
+
editor.get_context_at_cursor = editor._original_get_context_at_cursor
|
|
231
|
+
delattr(editor, '_original_get_context_at_cursor')
|
|
232
|
+
|
|
233
|
+
if hasattr(editor, '_original_complete'):
|
|
234
|
+
editor.complete = editor._original_complete
|
|
235
|
+
delattr(editor, '_original_complete')
|
|
236
|
+
|
|
237
|
+
# Remove from tracked collections
|
|
238
|
+
del self._editors[editor_id]
|
|
239
|
+
|
|
240
|
+
if editor_id in self._completers:
|
|
241
|
+
del self._completers[editor_id]
|
|
242
|
+
|
|
243
|
+
def _on_ai_suggestion_ready(self, suggestion: str, cursor_position: int):
|
|
244
|
+
"""Handle AI suggestion ready signal."""
|
|
245
|
+
print(f"[AI] Signal received: '{suggestion[:30]}...' for position {cursor_position}")
|
|
246
|
+
|
|
247
|
+
if not suggestion or not self._pending_ai_editor_id:
|
|
248
|
+
print(f"[AI] Skipping: no suggestion or no pending editor (editor_id: {self._pending_ai_editor_id})")
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
# Find the editor that requested the suggestion
|
|
252
|
+
# The pending_ai_editor_id is now stored as the actual editor object id
|
|
253
|
+
editor = None
|
|
254
|
+
for editor_key, stored_editor in self._editors.items():
|
|
255
|
+
if id(stored_editor) == self._pending_ai_editor_id:
|
|
256
|
+
editor = stored_editor
|
|
257
|
+
break
|
|
258
|
+
|
|
259
|
+
if not editor:
|
|
260
|
+
print(f"[AI] Editor not found for id: {self._pending_ai_editor_id}")
|
|
261
|
+
print(f"[AI] Available editors: {[(k, id(v)) for k, v in self._editors.items()]}")
|
|
262
|
+
return
|
|
263
|
+
|
|
264
|
+
# Verify cursor is still at the expected position
|
|
265
|
+
current_position = editor.textCursor().position()
|
|
266
|
+
if abs(current_position - cursor_position) > 5: # Allow small tolerance
|
|
267
|
+
print(f"[AI] Cursor moved too much: expected ~{cursor_position}, got {current_position}")
|
|
268
|
+
return # Cursor moved too much, discard suggestion
|
|
269
|
+
|
|
270
|
+
# Show the AI suggestion as ghost text
|
|
271
|
+
if hasattr(editor, 'show_ghost_text'):
|
|
272
|
+
print(f"[AI] Showing ghost text: '{suggestion[:30]}...'")
|
|
273
|
+
# Mark this as an AI suggestion (could be used to show different styling)
|
|
274
|
+
editor._is_ai_suggestion = True
|
|
275
|
+
editor.show_ghost_text(suggestion, current_position)
|
|
276
|
+
else:
|
|
277
|
+
print(f"[AI] Editor doesn't have show_ghost_text method")
|
|
278
|
+
|
|
279
|
+
def update_schema(self, tables, table_columns, column_types=None):
|
|
280
|
+
"""
|
|
281
|
+
Update schema information for all registered editors.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
tables: Set of table names
|
|
285
|
+
table_columns: Dictionary mapping table names to column lists
|
|
286
|
+
column_types: Optional dictionary of column data types
|
|
287
|
+
"""
|
|
288
|
+
# Update the context suggester with new schema information
|
|
289
|
+
self.suggester.update_schema(tables, table_columns, column_types)
|
|
290
|
+
|
|
291
|
+
# Also update AI manager with schema context
|
|
292
|
+
self._ai_manager.update_schema_context(list(tables), table_columns)
|
|
293
|
+
|
|
294
|
+
def record_query(self, query_text):
|
|
295
|
+
"""
|
|
296
|
+
Record a query to improve suggestion relevance.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
query_text: The SQL query to record
|
|
300
|
+
"""
|
|
301
|
+
self.suggester.record_query(query_text)
|
|
302
|
+
|
|
303
|
+
def get_suggestions(self, text_before_cursor, current_word=""):
|
|
304
|
+
"""
|
|
305
|
+
Get context-aware suggestions for the given text context.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
text_before_cursor: Text from start of document to cursor position
|
|
309
|
+
current_word: The current word being typed (possibly empty)
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
List of suggestion strings relevant to the current context
|
|
313
|
+
"""
|
|
314
|
+
return self.suggester.get_suggestions(text_before_cursor, current_word)
|
|
315
|
+
|
|
316
|
+
def update_all_completers(self):
|
|
317
|
+
"""Update all registered completers with current schema and usage data."""
|
|
318
|
+
for editor_id, editor in self._editors.items():
|
|
319
|
+
# Force a completion update next time complete() is called
|
|
320
|
+
if hasattr(editor, '_context_cache'):
|
|
321
|
+
editor._context_cache = {}
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# Create a singleton instance to be used application-wide
|
|
325
|
+
suggestion_manager = SuggestionManager()
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def get_suggestion_manager():
|
|
329
|
+
"""Get the global suggestion manager instance."""
|
|
330
|
+
return suggestion_manager
|