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.
Files changed (54) hide show
  1. sqlshell/__init__.py +84 -0
  2. sqlshell/__main__.py +4926 -0
  3. sqlshell/ai_autocomplete.py +392 -0
  4. sqlshell/ai_settings_dialog.py +337 -0
  5. sqlshell/context_suggester.py +768 -0
  6. sqlshell/create_test_data.py +152 -0
  7. sqlshell/data/create_test_data.py +137 -0
  8. sqlshell/db/__init__.py +6 -0
  9. sqlshell/db/database_manager.py +1318 -0
  10. sqlshell/db/export_manager.py +188 -0
  11. sqlshell/editor.py +1166 -0
  12. sqlshell/editor_integration.py +127 -0
  13. sqlshell/execution_handler.py +421 -0
  14. sqlshell/menus.py +262 -0
  15. sqlshell/notification_manager.py +370 -0
  16. sqlshell/query_tab.py +904 -0
  17. sqlshell/resources/__init__.py +1 -0
  18. sqlshell/resources/icon.png +0 -0
  19. sqlshell/resources/logo_large.png +0 -0
  20. sqlshell/resources/logo_medium.png +0 -0
  21. sqlshell/resources/logo_small.png +0 -0
  22. sqlshell/resources/splash_screen.gif +0 -0
  23. sqlshell/space_invaders.py +501 -0
  24. sqlshell/splash_screen.py +405 -0
  25. sqlshell/sqlshell/__init__.py +5 -0
  26. sqlshell/sqlshell/create_test_data.py +118 -0
  27. sqlshell/sqlshell/create_test_databases.py +96 -0
  28. sqlshell/sqlshell_demo.png +0 -0
  29. sqlshell/styles.py +257 -0
  30. sqlshell/suggester_integration.py +330 -0
  31. sqlshell/syntax_highlighter.py +124 -0
  32. sqlshell/table_list.py +996 -0
  33. sqlshell/ui/__init__.py +6 -0
  34. sqlshell/ui/bar_chart_delegate.py +49 -0
  35. sqlshell/ui/filter_header.py +469 -0
  36. sqlshell/utils/__init__.py +16 -0
  37. sqlshell/utils/profile_cn2.py +1661 -0
  38. sqlshell/utils/profile_column.py +2635 -0
  39. sqlshell/utils/profile_distributions.py +616 -0
  40. sqlshell/utils/profile_entropy.py +347 -0
  41. sqlshell/utils/profile_foreign_keys.py +779 -0
  42. sqlshell/utils/profile_keys.py +2834 -0
  43. sqlshell/utils/profile_ohe.py +934 -0
  44. sqlshell/utils/profile_ohe_advanced.py +754 -0
  45. sqlshell/utils/profile_ohe_comparison.py +237 -0
  46. sqlshell/utils/profile_prediction.py +926 -0
  47. sqlshell/utils/profile_similarity.py +876 -0
  48. sqlshell/utils/search_in_df.py +90 -0
  49. sqlshell/widgets.py +400 -0
  50. sqlshell-0.4.4.dist-info/METADATA +441 -0
  51. sqlshell-0.4.4.dist-info/RECORD +54 -0
  52. sqlshell-0.4.4.dist-info/WHEEL +5 -0
  53. sqlshell-0.4.4.dist-info/entry_points.txt +2 -0
  54. 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