Files

307 lines
10 KiB
Python

"""
Edit Tool - Semantic refactoring via LSP
========================================
Purpose: semantic refactors via LSP across languages.
Operations:
- rename: Rename symbol across workspace
- code_action: Apply LSP code actions
- organize_imports: Organize imports
- apply_workspace_edit: Apply arbitrary WorkspaceEdit
Supports gopls, tsserver, pyright, rust-analyzer, clangd
"""
from typing import Any, Dict, List, Optional
from .dev_tools import DevResult, DevToolBase, create_dev_result
class EditTool(DevToolBase):
"""LSP-powered semantic editing tool"""
def __init__(self, target: str, op: str, **kwargs):
super().__init__(target, **kwargs)
self.op = op
self.file = kwargs.get("file")
self.pos = kwargs.get("pos") # {line: int, character: int}
self.range = kwargs.get("range") # {start: {line, ch}, end: {line, ch}}
self.new_name = kwargs.get("new_name")
self.only = kwargs.get("only", []) # code action kinds
self.apply = kwargs.get("apply", True)
async def execute(self) -> DevResult:
"""Execute edit operation"""
try:
if self.op == "rename":
return await self._rename()
elif self.op == "code_action":
return await self._code_action()
elif self.op == "organize_imports":
return await self._organize_imports()
elif self.op == "apply_workspace_edit":
return await self._apply_workspace_edit()
else:
return create_dev_result(
ok=False,
root=self.workspace["root"],
language_used=self.language,
backend_used=self.backend,
scope_resolved=self.target,
errors=[f"Unknown operation: {self.op}"],
)
except Exception as e:
return create_dev_result(
ok=False,
root=self.workspace["root"],
language_used=self.language,
backend_used=self.backend,
scope_resolved=self.target,
errors=[str(e)],
)
async def _rename(self) -> DevResult:
"""Rename symbol using LSP"""
if not self.file or not self.pos or not self.new_name:
return create_dev_result(
ok=False,
root=self.workspace["root"],
language_used=self.language,
backend_used=self.backend,
scope_resolved=self.target,
errors=["rename requires file, pos, and new_name"],
)
# Use appropriate LSP client based on language
if self.language == "go":
return await self._gopls_rename()
elif self.language == "ts":
return await self._typescript_rename()
elif self.language == "py":
return await self._pyright_rename()
elif self.language == "rs":
return await self._rust_analyzer_rename()
else:
return create_dev_result(
ok=False,
root=self.workspace["root"],
language_used=self.language,
backend_used=self.backend,
scope_resolved=self.target,
errors=[f"Rename not supported for language: {self.language}"],
)
async def _code_action(self) -> DevResult:
"""Apply code actions"""
if self.language == "go":
return await self._gopls_code_action()
elif self.language == "ts":
return await self._typescript_code_action()
elif self.language == "py":
return await self._pyright_code_action()
else:
return create_dev_result(
ok=False,
root=self.workspace["root"],
language_used=self.language,
backend_used=self.backend,
scope_resolved=self.target,
errors=[f"Code actions not supported for language: {self.language}"],
)
async def _organize_imports(self) -> DevResult:
"""Organize imports"""
if self.language == "go":
# Use goimports directly
result = self._run_command(["goimports", "-w"] + self.resolved["files"])
elif self.language == "ts":
# Use TypeScript organize imports
return await self._typescript_organize_imports()
elif self.language == "py":
# Use ruff or isort
result = self._run_command(
["ruff", "check", "--select", "I", "--fix"] + self.resolved["files"]
)
else:
return create_dev_result(
ok=False,
root=self.workspace["root"],
language_used=self.language,
backend_used=self.backend,
scope_resolved=self.target,
errors=[
f"Organize imports not supported for language: {self.language}"
],
)
return create_dev_result(
ok=result.returncode == 0,
root=self.workspace["root"],
language_used=self.language,
backend_used=self.backend,
scope_resolved=self.resolved["scope"],
stdout=result.stdout,
stderr=result.stderr,
exit_code=result.returncode,
touched_files=self.resolved["files"] if result.returncode == 0 else [],
)
async def _gopls_rename(self) -> DevResult:
"""Rename using gopls"""
# Use gopls command line
cmd = [
"gopls",
"rename",
f"-w={self.workspace['root']}",
f"{self.file}:{self.pos['line'] + 1}:{self.pos['character'] + 1}",
self.new_name,
]
result = self._run_command(cmd)
# Parse touched files from output if available
touched_files = []
if result.returncode == 0:
# gopls might output changed files - parse if available
touched_files = self.resolved["files"]
return create_dev_result(
ok=result.returncode == 0,
root=self.workspace["root"],
language_used=self.language,
backend_used="gopls",
scope_resolved=self.resolved["scope"],
stdout=result.stdout,
stderr=result.stderr,
exit_code=result.returncode,
touched_files=touched_files,
)
async def _typescript_rename(self) -> DevResult:
"""Rename using TypeScript Language Server"""
# For now, use a simple approach - could integrate with actual TS LSP
return create_dev_result(
ok=False,
root=self.workspace["root"],
language_used=self.language,
backend_used="tsserver",
scope_resolved=self.target,
errors=["TypeScript LSP rename not implemented yet - use IDE"],
)
async def _pyright_rename(self) -> DevResult:
"""Rename using Pyright"""
return create_dev_result(
ok=False,
root=self.workspace["root"],
language_used=self.language,
backend_used="pyright",
scope_resolved=self.target,
errors=["Pyright rename not implemented yet - use IDE"],
)
async def _rust_analyzer_rename(self) -> DevResult:
"""Rename using rust-analyzer"""
return create_dev_result(
ok=False,
root=self.workspace["root"],
language_used=self.language,
backend_used="rust-analyzer",
scope_resolved=self.target,
errors=["rust-analyzer rename not implemented yet - use IDE"],
)
async def _gopls_code_action(self) -> DevResult:
"""Apply code actions using gopls"""
return create_dev_result(
ok=False,
root=self.workspace["root"],
language_used=self.language,
backend_used="gopls",
scope_resolved=self.target,
errors=["gopls code actions not implemented yet"],
)
async def _typescript_code_action(self) -> DevResult:
"""Apply code actions using TypeScript"""
return create_dev_result(
ok=False,
root=self.workspace["root"],
language_used=self.language,
backend_used="tsserver",
scope_resolved=self.target,
errors=["TypeScript code actions not implemented yet"],
)
async def _pyright_code_action(self) -> DevResult:
"""Apply code actions using Pyright"""
return create_dev_result(
ok=False,
root=self.workspace["root"],
language_used=self.language,
backend_used="pyright",
scope_resolved=self.target,
errors=["Pyright code actions not implemented yet"],
)
async def _typescript_organize_imports(self) -> DevResult:
"""Organize TypeScript imports"""
return create_dev_result(
ok=False,
root=self.workspace["root"],
language_used=self.language,
backend_used="tsserver",
scope_resolved=self.target,
errors=["TypeScript organize imports not implemented yet"],
)
async def _apply_workspace_edit(self) -> DevResult:
"""Apply arbitrary workspace edit"""
return create_dev_result(
ok=False,
root=self.workspace["root"],
language_used=self.language,
backend_used=self.backend,
scope_resolved=self.target,
errors=["apply_workspace_edit not implemented yet"],
)
# MCP tool integration
async def edit_tool_handler(
target: str,
op: str,
file: Optional[str] = None,
pos: Optional[Dict[str, int]] = None,
range: Optional[Dict[str, Any]] = None,
new_name: Optional[str] = None,
only: Optional[List[str]] = None,
apply: bool = True,
language: str = "auto",
backend: str = "auto",
root: Optional[str] = None,
env: Optional[Dict[str, str]] = None,
dry_run: bool = False,
) -> Dict[str, Any]:
"""MCP handler for edit tool"""
tool = EditTool(
target=target,
op=op,
file=file,
pos=pos,
range=range,
new_name=new_name,
only=only or [],
apply=apply,
language=language,
backend=backend,
root=root,
env=env,
dry_run=dry_run,
)
result = await tool.execute()
return result.dict()