Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions aeon/lsp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
DidChangeWatchedFilesParams,
DidOpenTextDocumentParams,
Diagnostic,
ExecuteCommandParams,
MessageType,
PublishDiagnosticsParams,
Range,
Expand All @@ -45,6 +44,7 @@ def __init__(self, aeon_driver: AeonDriver):
def start(self, tcp_server):
if not tcp_server:
self.start_io()
return

host, port = tcp_server.split(":") if ":" in tcp_server else ("localhost", tcp_server)

Expand Down Expand Up @@ -148,15 +148,10 @@ async def code_action(
@self.command(SYNTHESIZE_COMMAND)
async def execute_synthesize(
ls: AeonLanguageServer,
params: ExecuteCommandParams,
uri: str,
hole_name_str: str,
synthesizer_name: str,
) -> None:
args = params.arguments or []
if len(args) < 3:
ls.show_message("aeon.synthesize requires [uri, hole_name, synthesizer]", MessageType.Error)
return

uri, hole_name_str, synthesizer_name = args[0], args[1], args[2]

loop = asyncio.get_event_loop()
try:
result = await loop.run_in_executor(
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "AeonLang"
version = "4.0.5b0"
version = "4.0.5"
description = "Language with Refinement Types"
authors = [
{ name = "Alcides Fonseca", email = "me@alcidesfonseca.com" }
Expand Down
86 changes: 84 additions & 2 deletions tests/lsp_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import pytest
from lsprotocol.types import MessageType, Position, Range
from lsprotocol.types import CodeAction, CodeActionKind, Command, MessageType, Position, Range

from aeon.facade.driver import AeonConfig, AeonDriver
from aeon.lsp.aeon_adapter import HolePosition, ParseResult, find_holes_in_source
from aeon.lsp.server import SYNTHESIZERS, AeonLanguageServer, _run_synthesis
from aeon.lsp.server import SYNTHESIZERS, SYNTHESIZE_COMMAND, AeonLanguageServer, _run_synthesis
from aeon.logger.logger import setup_logger
from aeon.synthesis.uis.api import SilentSynthesisUI

Expand Down Expand Up @@ -248,3 +248,85 @@ def test_run_synthesis_unknown_synthesizer():

assert result is None
assert any("synthesizer" in msg.lower() or "unknown" in msg.lower() for msg, _ in mock_ls.messages)


# ---------------------------------------------------------------------------
# code_action: one action per synthesizer
# ---------------------------------------------------------------------------


def _build_code_actions(hole: HolePosition, uri: str) -> list[CodeAction]:
"""Mirrors the action-building loop inside the code_action handler."""
actions = []
for synthesizer in SYNTHESIZERS:
action = CodeAction(
title=f"Synthesize ?{hole.name} with {synthesizer}",
kind=CodeActionKind.RefactorRewrite,
command=Command(
title=f"Synthesize ?{hole.name} with {synthesizer}",
command=SYNTHESIZE_COMMAND,
arguments=[uri, hole.name, synthesizer],
),
)
actions.append(action)
return actions


def test_code_action_creates_one_action_per_synthesizer():
hole = HolePosition(name="hole", range=make_range(0, 14, 0, 19))
actions = _build_code_actions(hole, "file:///test.ae")

assert len(actions) == len(SYNTHESIZERS)


def test_code_action_titles_contain_synthesizer_names():
hole = HolePosition(name="hole", range=make_range(0, 14, 0, 19))
actions = _build_code_actions(hole, "file:///test.ae")

titles = [a.title for a in actions]
for synthesizer in SYNTHESIZERS:
assert any(synthesizer in t for t in titles)


def test_code_action_commands_have_correct_arguments():
uri = "file:///test.ae"
hole = HolePosition(name="myhole", range=make_range(1, 4, 1, 11))
actions = _build_code_actions(hole, uri)

for action, synthesizer in zip(actions, SYNTHESIZERS):
args = action.command.arguments
assert args[0] == uri
assert args[1] == "myhole"
assert args[2] == synthesizer


# ---------------------------------------------------------------------------
# _run_synthesis parametrized over all synthesizers
# ---------------------------------------------------------------------------


class _FakeOllamaResponse:
response = ""


@pytest.mark.parametrize("synthesizer", SYNTHESIZERS)
def test_run_synthesis_each_synthesizer(synthesizer, monkeypatch):
source = "def synth : Int = ?hole;"
mock_ls = MockLS(source)
driver = make_driver()

if synthesizer == "llm":
monkeypatch.setattr("ollama.generate", lambda **kwargs: _FakeOllamaResponse())
result = _run_synthesis(driver, mock_ls, "file:///test.ae", "hole", synthesizer)
# With a blank response the LLM synthesizer cannot find a valid term;
# we only verify it completes without raising an exception.
assert result is None or isinstance(result, tuple)
return

result = _run_synthesis(driver, mock_ls, "file:///test.ae", "hole", synthesizer)

assert result is not None, f"Synthesizer '{synthesizer}' returned None. Messages: {mock_ls.messages}"
synthesized_str, hole_range = result
assert isinstance(synthesized_str, str) and len(synthesized_str) > 0
assert hole_range.start.line == 0
assert hole_range.start.character == source.index("?")
Loading