Skip to content

Commit 0f252d8

Browse files
committed
feat: integrate autoformatting in WriteFile and EditFile commands
We're going to integrate autoformatting into the WriteFile and EditFile commands. After doing the file write with these commands, run the "format" command (as defined by RunCommand) if it exists. You should NOT run the pre/post commit associated with RunCommand (because the formatting should be directly integrated with the edit/write), so make sure you hook into the run command infrastructure at the correct spot. ```git-revs 3f698ba (Base revision) c56fb59 Add run_formatter_without_commit to __all__ 3a9183d Add run_formatter_without_commit function to format a specific file without commit operations 13c9c8d Add run_formatter_without_commit import to write_file.py d5459f2 Add autoformatting to write_file_content function 4d419ac Add run_formatter_without_commit import to edit_file.py 2ac534f Add autoformatting to edit_file_content function 77cf162 Improve run_formatter_without_commit to handle different formatter types HEAD Auto-commit format changes ``` codemcp-id: 280-feat-integrate-autoformatting-in-writefile-and-edi ghstack-source-id: 5815be4 Pull-Request-resolved: #273
1 parent 3aaa4a8 commit 0f252d8

File tree

4 files changed

+171
-5
lines changed

4 files changed

+171
-5
lines changed

codemcp/code_command.py

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import logging
44
import os
55
import subprocess
6-
from typing import Any, Dict, List, Optional, cast
6+
from typing import Any, Dict, List, Optional, Tuple, cast
77

88
import tomli
99

@@ -15,6 +15,7 @@
1515
"get_command_from_config",
1616
"check_for_changes",
1717
"run_code_command",
18+
"run_formatter_without_commit",
1819
]
1920

2021

@@ -215,3 +216,120 @@ async def run_code_command(
215216
error_msg = f"Error during {command_name}: {e}"
216217
logging.error(error_msg)
217218
return f"Error: {error_msg}"
219+
220+
221+
async def run_formatter_without_commit(file_path: str) -> Tuple[bool, str]:
222+
"""Run the formatter on a specific file without performing pre/post commit operations.
223+
224+
This function attempts to be flexible in working with different formatter configurations:
225+
1. If the formatter is configured to run on specific files (like black path/to/file.py)
226+
2. If the formatter is configured to run on all files in a directory
227+
228+
Args:
229+
file_path: Absolute path to the file to format
230+
231+
Returns:
232+
A tuple containing (success_status, message)
233+
"""
234+
try:
235+
# Get the project directory (repository root)
236+
project_dir = os.path.dirname(file_path)
237+
try:
238+
project_dir = await get_repository_root(project_dir)
239+
except (subprocess.SubprocessError, OSError, ValueError) as e:
240+
logging.debug(f"Not in a git repository: {e}")
241+
# Fall back to the directory containing the file
242+
pass
243+
244+
# Get the format command from config
245+
format_command = get_command_from_config(project_dir, "format")
246+
if not format_command:
247+
return False, "No format command configured in codemcp.toml"
248+
249+
# Use relative path from project_dir for the formatting command
250+
rel_path = os.path.relpath(file_path, project_dir)
251+
252+
# First try running the formatter with the specific file path
253+
# This works with tools like black, prettier, etc. that accept file paths
254+
try:
255+
specific_command = format_command.copy() + [rel_path]
256+
257+
result = await run_command(
258+
specific_command,
259+
cwd=project_dir,
260+
check=True,
261+
capture_output=True,
262+
text=True,
263+
)
264+
265+
# If we get here, the formatter successfully ran on the specific file
266+
truncated_stdout = truncate_output_content(result.stdout, prefer_end=True)
267+
return True, f"File formatted successfully:\n{truncated_stdout}"
268+
except subprocess.CalledProcessError as e:
269+
# If the specific file approach failed, try running the formatter without arguments
270+
# This might work for formatters that automatically detect files to format
271+
logging.debug(
272+
f"Formatter failed with specific file, trying without file path"
273+
)
274+
try:
275+
# Run the formatter without specific file arguments
276+
# Some formatters automatically find and format files
277+
result = await run_command(
278+
format_command,
279+
cwd=project_dir,
280+
check=True,
281+
capture_output=True,
282+
text=True,
283+
)
284+
285+
# If we get here, the formatter ran successfully
286+
truncated_stdout = truncate_output_content(
287+
result.stdout, prefer_end=True
288+
)
289+
return True, f"File formatted successfully:\n{truncated_stdout}"
290+
except subprocess.CalledProcessError:
291+
# Both approaches failed, return error from the first attempt
292+
# as it's more likely to be relevant to the specific file
293+
truncated_stdout = truncate_output_content(
294+
e.output if e.output else "", prefer_end=True
295+
)
296+
truncated_stderr = truncate_output_content(
297+
e.stderr if e.stderr else "", prefer_end=True
298+
)
299+
300+
# Include both stdout and stderr in the error message
301+
stdout_info = (
302+
f"STDOUT:\n{truncated_stdout}"
303+
if truncated_stdout
304+
else "STDOUT: <empty>"
305+
)
306+
stderr_info = (
307+
f"STDERR:\n{truncated_stderr}"
308+
if truncated_stderr
309+
else "STDERR: <empty>"
310+
)
311+
return False, f"Formatter failed: {stdout_info}\n{stderr_info}"
312+
except subprocess.CalledProcessError as e:
313+
# Truncate stdout and stderr if needed
314+
truncated_stdout = truncate_output_content(
315+
e.output if e.output else "", prefer_end=True
316+
)
317+
truncated_stderr = truncate_output_content(
318+
e.stderr if e.stderr else "", prefer_end=True
319+
)
320+
321+
# Include both stdout and stderr in the error message
322+
stdout_info = (
323+
f"STDOUT:\n{truncated_stdout}" if truncated_stdout else "STDOUT: <empty>"
324+
)
325+
stderr_info = (
326+
f"STDERR:\n{truncated_stderr}" if truncated_stderr else "STDERR: <empty>"
327+
)
328+
error_msg = f"Format command failed with exit code {e.returncode}:\n{stdout_info}\n{stderr_info}"
329+
330+
logging.error(f"Format command failed with exit code {e.returncode}")
331+
return False, f"Error: {error_msg}"
332+
except Exception as e:
333+
error_msg = f"Error during formatting: {e}"
334+
logging.error(error_msg)
335+
return False, f"Error: {error_msg}"

codemcp/tools/edit_file.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from difflib import SequenceMatcher
1010
from typing import Any, Dict, List, Tuple
1111

12+
from ..code_command import run_formatter_without_commit
1213
from ..common import get_edit_snippet
1314
from ..file_utils import (
1415
async_open_text,
@@ -773,6 +774,22 @@ async def edit_file_content(
773774
if read_file_timestamps is not None:
774775
read_file_timestamps[full_file_path] = os.stat(full_file_path).st_mtime
775776

777+
# Try to run the formatter on the file
778+
format_message = ""
779+
formatter_success, formatter_output = await run_formatter_without_commit(
780+
full_file_path
781+
)
782+
if formatter_success:
783+
logger.info(f"Auto-formatted {full_file_path}")
784+
if formatter_output.strip():
785+
format_message = "\nAuto-formatted the file"
786+
else:
787+
# Only log warning if there was actually a format command configured but it failed
788+
if not "No format command configured" in formatter_output:
789+
logger.warning(
790+
f"Failed to auto-format {full_file_path}: {formatter_output}"
791+
)
792+
776793
# Generate a snippet of the edited file to show in the response
777794
snippet = get_edit_snippet(content, old_string, new_string)
778795

@@ -787,4 +804,4 @@ async def edit_file_content(
787804
else:
788805
git_message = f"\n\nFailed to commit changes to git: {message}"
789806

790-
return f"Successfully edited {full_file_path}\n\nHere's a snippet of the edited file:\n{snippet}{git_message}"
807+
return f"Successfully edited {full_file_path}\n\nHere's a snippet of the edited file:\n{snippet}{format_message}{git_message}"

codemcp/tools/write_file.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#!/usr/bin/env python3
22

3+
import logging
34
import os
45

6+
from ..code_command import run_formatter_without_commit
57
from ..file_utils import (
68
check_file_path_and_permissions,
79
check_git_tracking_for_existing_file,
@@ -61,6 +63,18 @@ async def write_file_content(
6163
# Write the content with UTF-8 encoding and proper line endings
6264
await write_text_content(file_path, content, "utf-8", line_endings)
6365

66+
# Try to run the formatter on the file
67+
format_message = ""
68+
formatter_success, formatter_output = await run_formatter_without_commit(file_path)
69+
if formatter_success:
70+
logging.info(f"Auto-formatted {file_path}")
71+
if formatter_output.strip():
72+
format_message = f"\nAuto-formatted the file"
73+
else:
74+
# Only log warning if there was actually a format command configured but it failed
75+
if not "No format command configured" in formatter_output:
76+
logging.warning(f"Failed to auto-format {file_path}: {formatter_output}")
77+
6478
# Commit the changes
6579
git_message = ""
6680
success, message = await commit_changes(file_path, description, chat_id)
@@ -69,4 +83,4 @@ async def write_file_content(
6983
else:
7084
git_message = f"\nFailed to commit changes to git: {message}"
7185

72-
return f"Successfully wrote to {file_path}{git_message}"
86+
return f"Successfully wrote to {file_path}{format_message}{git_message}"

e2e/test_run_command.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,16 @@ def test_run_command_with_args(project_dir):
6060
f.write("test content")
6161

6262
result = subprocess.run(
63-
[sys.executable, "-m", "codemcp", "run", "list", test_file, "--path", project_dir],
63+
[
64+
sys.executable,
65+
"-m",
66+
"codemcp",
67+
"run",
68+
"list",
69+
test_file,
70+
"--path",
71+
project_dir,
72+
],
6473
capture_output=True,
6574
text=True,
6675
check=True,
@@ -72,7 +81,15 @@ def test_run_command_error_exit_code(project_dir):
7281
"""Test that error exit codes from the command are propagated."""
7382
# This should return a non-zero exit code
7483
process = subprocess.run(
75-
[sys.executable, "-m", "codemcp", "run", "exit_with_error", "--path", project_dir],
84+
[
85+
sys.executable,
86+
"-m",
87+
"codemcp",
88+
"run",
89+
"exit_with_error",
90+
"--path",
91+
project_dir,
92+
],
7693
capture_output=True,
7794
text=True,
7895
check=False,

0 commit comments

Comments
 (0)