Skip to content

Commit 7c85db4

Browse files
authored
Merge pull request #367 from capocchi/version-5.1
add static autocompletion
2 parents a21150d + 4dbb01b commit 7c85db4

File tree

1 file changed

+163
-0
lines changed

1 file changed

+163
-0
lines changed

devsimpy/Editor.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@
3636
import traceback
3737
import collections
3838

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+
3947
import inspect
4048
if not hasattr(inspect, 'getargspec'):
4149
inspect.getargspec = inspect.getfullargspec
@@ -349,6 +357,160 @@ def __init__(self, parent, ID, pos=wx.DefaultPosition, size=wx.DefaultSize, styl
349357

350358
self.SetCaretForeground("BLUE")
351359

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+
352514
# NOTE: PythonSTC :: __str__ => String representation of the class
353515
@classmethod
354516
def __str__(cls):
@@ -736,6 +898,7 @@ def SetUpEditor(self):
736898
self.SetSelBackground(True, wx.SystemSettings_GetColour(wx.SYS_COLOUR_HIGHLIGHT) )
737899
self.SetSelForeground(True, wx.SystemSettings_GetColour(wx.SYS_COLOUR_HIGHLIGHTTEXT))
738900

901+
739902
### NOTE: CodeEditor :: RegisterModifiedEvent => todo
740903
def RegisterModifiedEvent(self, eventHandler):
741904
"""

0 commit comments

Comments
 (0)