diff --git a/aeon/lsp/server.py b/aeon/lsp/server.py index 82451fe8..6d18ec50 100644 --- a/aeon/lsp/server.py +++ b/aeon/lsp/server.py @@ -20,7 +20,6 @@ DidChangeWatchedFilesParams, DidOpenTextDocumentParams, Diagnostic, - ExecuteCommandParams, MessageType, PublishDiagnosticsParams, Range, @@ -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) @@ -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( diff --git a/pyproject.toml b/pyproject.toml index 4d249a0c..631db465 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" } diff --git a/tests/lsp_test.py b/tests/lsp_test.py index b49003fb..cd7907a4 100644 --- a/tests/lsp_test.py +++ b/tests/lsp_test.py @@ -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 @@ -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("?")