|
36 | 36 | import traceback |
37 | 37 | import collections |
38 | 38 |
|
| 39 | +import subprocess |
| 40 | +import requests |
| 41 | +import time |
| 42 | + |
| 43 | +OLLAMA_PORT = 11434 |
| 44 | +OLLAMA_MODEL = "mistral" |
| 45 | +OLLAMA_SERVER_CMD = ["ollama", "serve"] |
| 46 | + |
39 | 47 | import inspect |
40 | 48 | if not hasattr(inspect, 'getargspec'): |
41 | 49 | inspect.getargspec = inspect.getfullargspec |
@@ -349,6 +357,160 @@ def __init__(self, parent, ID, pos=wx.DefaultPosition, size=wx.DefaultSize, styl |
349 | 357 |
|
350 | 358 | self.SetCaretForeground("BLUE") |
351 | 359 |
|
| 360 | + # Associer les événements clavier |
| 361 | + self.Bind(wx.EVT_KEY_UP, self.on_key_up) |
| 362 | + |
| 363 | + # Analyse statique |
| 364 | + self.classes = {} # nom_classe -> set(attributs/méthodes) |
| 365 | + self.instances = {} # nom_instance -> nom_classe |
| 366 | + |
| 367 | + |
| 368 | + # ------------------ Ollama ------------------ |
| 369 | + def start_ollama_server(self): |
| 370 | + try: |
| 371 | + self.ollama_process = subprocess.Popen( |
| 372 | + OLLAMA_SERVER_CMD, |
| 373 | + stdout=subprocess.PIPE, |
| 374 | + stderr=subprocess.PIPE |
| 375 | + ) |
| 376 | + # Attendre qu'il soit prêt dans un thread |
| 377 | + threading.Thread(target=self.wait_for_server, daemon=True).start() |
| 378 | + except Exception as e: |
| 379 | + print("Erreur démarrage Ollama:", e) |
| 380 | + |
| 381 | + def wait_for_server(self, timeout=10): |
| 382 | + url = f"http://localhost:{OLLAMA_PORT}/api/generate" |
| 383 | + start = time.time() |
| 384 | + while time.time() - start < timeout: |
| 385 | + try: |
| 386 | + requests.get(url, timeout=0.5) |
| 387 | + except requests.exceptions.RequestException: |
| 388 | + time.sleep(0.5) |
| 389 | + else: |
| 390 | + self.ollama_ready = True |
| 391 | + print("Serveur Ollama prêt !") |
| 392 | + return |
| 393 | + print("Impossible de joindre le serveur Ollama") |
| 394 | + self.ollama_ready = False |
| 395 | + |
| 396 | + def fetch_ollama_suggestions(self, prompt, length): |
| 397 | + if not self.ollama_ready: |
| 398 | + return |
| 399 | + def worker(): |
| 400 | + try: |
| 401 | + response = requests.post( |
| 402 | + f"http://localhost:{OLLAMA_PORT}/api/generate", |
| 403 | + json={"model": OLLAMA_MODEL, "prompt": prompt}, |
| 404 | + timeout=30 # plus long |
| 405 | + ) |
| 406 | + response.raise_for_status() |
| 407 | + text = response.json().get("text", "").strip() |
| 408 | + if text: |
| 409 | + self.AutoCompShow(length, text) |
| 410 | + except requests.exceptions.ReadTimeout: |
| 411 | + # Timeout temporaire, on ignore |
| 412 | + pass |
| 413 | + except Exception as e: |
| 414 | + print("Erreur Ollama:", e) |
| 415 | + threading.Thread(target=worker, daemon=True).start() |
| 416 | + |
| 417 | + # ------------------ Analyse statique ------------------ |
| 418 | + def update_classes_and_instances(self): |
| 419 | + self.classes.clear() |
| 420 | + self.instances.clear() |
| 421 | + text = self.GetText() |
| 422 | + lines = text.splitlines() |
| 423 | + |
| 424 | + class_name = None |
| 425 | + indent_level = 0 |
| 426 | + for line in lines: |
| 427 | + stripped = line.strip() |
| 428 | + # Début d'une classe |
| 429 | + class_match = re.match(r'class\s+(\w+)\s*[:\(]', stripped) |
| 430 | + if class_match: |
| 431 | + class_name = class_match.group(1) |
| 432 | + self.classes[class_name] = set() |
| 433 | + indent_level = len(line) - len(line.lstrip()) |
| 434 | + continue |
| 435 | + # Fin de classe |
| 436 | + if class_name and (len(line) - len(line.lstrip()) <= indent_level and stripped): |
| 437 | + class_name = None |
| 438 | + if class_name: |
| 439 | + # Méthodes |
| 440 | + method_match = re.match(r'def\s+(\w+)\s*\(', stripped) |
| 441 | + if method_match: |
| 442 | + self.classes[class_name].add(method_match.group(1)) |
| 443 | + # Attributs existants |
| 444 | + attr_match = re.match(r'self\.(\w+)', stripped) |
| 445 | + if attr_match: |
| 446 | + self.classes[class_name].add(attr_match.group(1)) |
| 447 | + |
| 448 | + # Détection instances |
| 449 | + for m in re.finditer(r'(\w+)\s*=\s*(\w+)\s*\(', text): |
| 450 | + var_name, cls_name = m.groups() |
| 451 | + if cls_name in self.classes: |
| 452 | + self.instances[var_name] = cls_name |
| 453 | + |
| 454 | + def get_current_class(self, pos): |
| 455 | + text_up_to_pos = self.GetTextRange(0, pos) |
| 456 | + lines = text_up_to_pos.splitlines() |
| 457 | + class_name = None |
| 458 | + indent_level = 0 |
| 459 | + for line in lines: |
| 460 | + stripped = line.strip() |
| 461 | + class_match = re.match(r'class\s+(\w+)\s*[:\(]', stripped) |
| 462 | + if class_match: |
| 463 | + class_name = class_match.group(1) |
| 464 | + indent_level = len(line) - len(line.lstrip()) |
| 465 | + continue |
| 466 | + if class_name and (len(line) - len(line.lstrip()) <= indent_level and stripped): |
| 467 | + class_name = None |
| 468 | + return class_name |
| 469 | + |
| 470 | + # ------------------ Événement frappe ------------------ |
| 471 | + def on_key_up(self, event): |
| 472 | + key = event.GetKeyCode() |
| 473 | + if (65 <= key <= 90) or (97 <= key <= 122) or key == ord('_') or key == ord('.'): |
| 474 | + pos = self.GetCurrentPos() |
| 475 | + start = self.WordStartPosition(pos, True) |
| 476 | + length = pos - start |
| 477 | + current_word = self.GetTextRange(start, pos) |
| 478 | + |
| 479 | + self.update_classes_and_instances() |
| 480 | + |
| 481 | + suggestions = set(keyword.kwlist) |
| 482 | + words = set(self.GetText().split()) |
| 483 | + suggestions |= words |
| 484 | + |
| 485 | + if '.' in current_word: |
| 486 | + obj_name, prefix = current_word.rsplit('.', 1) |
| 487 | + if obj_name == 'self': |
| 488 | + cls_name = self.get_current_class(pos) |
| 489 | + if cls_name and cls_name in self.classes: |
| 490 | + suggestions |= self.classes[cls_name] |
| 491 | + length = len(prefix) |
| 492 | + else: |
| 493 | + cls_name = self.instances.get(obj_name) |
| 494 | + if cls_name and cls_name in self.classes: |
| 495 | + suggestions |= self.classes[cls_name] |
| 496 | + length = len(prefix) |
| 497 | + |
| 498 | + # Affiche suggestions locales immédiatement |
| 499 | + self.AutoCompShow(length, " ".join(sorted(suggestions))) |
| 500 | + |
| 501 | + # Appel IA avec délai pour ne pas spammer |
| 502 | + # self.last_key_time = time.time() |
| 503 | + # threading.Thread(target=self.delayed_ia_call, args=(current_word, length), daemon=True).start() |
| 504 | + |
| 505 | + event.Skip() |
| 506 | + |
| 507 | + def delayed_ia_call(self, current_word, length): |
| 508 | + time.sleep(self.ia_delay) |
| 509 | + # Si aucune frappe depuis le délai |
| 510 | + if time.time() - self.last_key_time >= self.ia_delay: |
| 511 | + prompt = f"Complète le code suivant Python : {current_word}\n" |
| 512 | + self.fetch_ollama_suggestions(prompt, length) |
| 513 | + |
352 | 514 | # NOTE: PythonSTC :: __str__ => String representation of the class |
353 | 515 | @classmethod |
354 | 516 | def __str__(cls): |
@@ -736,6 +898,7 @@ def SetUpEditor(self): |
736 | 898 | self.SetSelBackground(True, wx.SystemSettings_GetColour(wx.SYS_COLOUR_HIGHLIGHT) ) |
737 | 899 | self.SetSelForeground(True, wx.SystemSettings_GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT)) |
738 | 900 |
|
| 901 | + |
739 | 902 | ### NOTE: CodeEditor :: RegisterModifiedEvent => todo |
740 | 903 | def RegisterModifiedEvent(self, eventHandler): |
741 | 904 | """ |
|
0 commit comments