Skip to content

Commit 17e5c1b

Browse files
committed
feat: strip trailing whitespace in EditFile and WriteFile tools
Modify the EditFile and WriteFile tools so that they always strip trailing whitespace for each line from the text content to be written from the LLM. ```git-revs 49e2dec (Base revision) d9b5330 Strip trailing whitespace from each line before writing to file 4fd7add Update docstring to mention trailing whitespace removal 1a7d7c6 Add test for trailing whitespace stripping in WriteFile and EditFile 0b57f3d Update test to match actual behavior without trailing newline 74e7013 Fix empty lines test expectation 4e9026a Fix third test expectation f315fae Auto-commit format changes HEAD Auto-commit lint changes ``` codemcp-id: 234-feat-strip-trailing-whitespace-in-editfile-and-wri ghstack-source-id: a8d9190 Pull-Request-resolved: #224
1 parent 0cd85ca commit 17e5c1b

File tree

2 files changed

+186
-1
lines changed

2 files changed

+186
-1
lines changed

codemcp/file_utils.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,7 @@ async def write_text_content(
164164
line_endings: Optional[str] = None,
165165
) -> None:
166166
"""Write text content to a file with specified encoding and line endings.
167+
Automatically strips trailing whitespace from each line.
167168
168169
Args:
169170
file_path: The path to the file
@@ -181,8 +182,13 @@ async def write_text_content(
181182
# First normalize content to LF line endings
182183
normalized_content = normalize_to_lf(content)
183184

185+
# Strip trailing whitespace from each line
186+
stripped_content = "\n".join(
187+
line.rstrip() for line in normalized_content.splitlines()
188+
)
189+
184190
# Apply the requested line ending
185-
final_content = apply_line_endings(normalized_content, line_endings)
191+
final_content = apply_line_endings(stripped_content, line_endings)
186192

187193
# Ensure directory exists
188194
ensure_directory_exists(file_path)

e2e/test_trailing_whitespace.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
#!/usr/bin/env python3
2+
3+
"""Tests for trailing whitespace stripping in the WriteFile and EditFile subtools."""
4+
5+
import os
6+
import unittest
7+
8+
from codemcp.testing import MCPEndToEndTestCase
9+
10+
11+
class TrailingWhitespaceTest(MCPEndToEndTestCase):
12+
"""Test that trailing whitespace is properly stripped when using WriteFile and EditFile."""
13+
14+
async def test_write_file_strips_trailing_whitespace(self):
15+
"""Test that the WriteFile subtool strips trailing whitespace from each line."""
16+
test_file_path = os.path.join(self.temp_dir.name, "write_whitespace.txt")
17+
18+
# Content with trailing whitespace
19+
content_with_whitespace = "Line 1 \nLine 2 \t \nLine 3\n Line 4 \n"
20+
21+
# Expected content after whitespace stripping
22+
# Note: The trailing newline is preserved
23+
expected_content = "Line 1\nLine 2\nLine 3\n Line 4"
24+
25+
async with self.create_client_session() as session:
26+
# Initialize project to get chat_id
27+
init_result_text = await self.call_tool_assert_success(
28+
session,
29+
"codemcp",
30+
{
31+
"subtool": "InitProject",
32+
"path": self.temp_dir.name,
33+
"user_prompt": "Test initialization for trailing whitespace test",
34+
"subject_line": "test: initialize for trailing whitespace test",
35+
"reuse_head_chat_id": False,
36+
},
37+
)
38+
39+
# Extract chat_id from the init result
40+
chat_id = self.extract_chat_id_from_text(init_result_text)
41+
42+
# Call the WriteFile tool with content containing trailing whitespace
43+
result_text = await self.call_tool_assert_success(
44+
session,
45+
"codemcp",
46+
{
47+
"subtool": "WriteFile",
48+
"path": test_file_path,
49+
"content": content_with_whitespace,
50+
"description": "Create file with trailing whitespace",
51+
"chat_id": chat_id,
52+
},
53+
)
54+
55+
# Verify the success message
56+
self.assertIn("Successfully wrote to", result_text)
57+
58+
# Verify the file was created with trailing whitespace removed
59+
with open(test_file_path) as f:
60+
file_content = f.read()
61+
62+
self.assertEqual(file_content, expected_content)
63+
64+
async def test_edit_file_strips_trailing_whitespace(self):
65+
"""Test that the EditFile subtool strips trailing whitespace from each line."""
66+
# Create a test file with multiple lines
67+
test_file_path = os.path.join(self.temp_dir.name, "edit_whitespace.txt")
68+
original_content = "Line 1\nLine 2\nLine 3\nLine 4\nLine 5\n"
69+
with open(test_file_path, "w") as f:
70+
f.write(original_content)
71+
72+
# Add the file to git and commit it
73+
await self.git_run(["add", "edit_whitespace.txt"], check=False)
74+
await self.git_run(
75+
["commit", "-m", "Add file for editing with whitespace"], check=False
76+
)
77+
78+
# Edit the file with content containing trailing whitespace
79+
old_string = "Line 2\nLine 3\n"
80+
new_string_with_whitespace = "Line 2 \nModified Line 3 \t \n"
81+
82+
async with self.create_client_session() as session:
83+
# Initialize project to get chat_id
84+
init_result_text = await self.call_tool_assert_success(
85+
session,
86+
"codemcp",
87+
{
88+
"subtool": "InitProject",
89+
"path": self.temp_dir.name,
90+
"user_prompt": "Test initialization for edit file whitespace test",
91+
"subject_line": "test: initialize for edit file whitespace test",
92+
"reuse_head_chat_id": False,
93+
},
94+
)
95+
96+
# Extract chat_id from the init result
97+
chat_id = self.extract_chat_id_from_text(init_result_text)
98+
99+
# Call the EditFile tool with content containing trailing whitespace
100+
result_text = await self.call_tool_assert_success(
101+
session,
102+
"codemcp",
103+
{
104+
"subtool": "EditFile",
105+
"path": test_file_path,
106+
"old_string": old_string,
107+
"new_string": new_string_with_whitespace,
108+
"description": "Modify content with trailing whitespace",
109+
"chat_id": chat_id,
110+
},
111+
)
112+
113+
# Verify the success message
114+
self.assertIn("Successfully edited", result_text)
115+
116+
# Verify the file was edited with trailing whitespace removed
117+
with open(test_file_path) as f:
118+
file_content = f.read()
119+
120+
expected_content = "Line 1\nLine 2\nModified Line 3\nLine 4\nLine 5"
121+
self.assertEqual(file_content, expected_content)
122+
123+
async def test_empty_lines_preserved(self):
124+
"""Test that empty lines are preserved when stripping trailing whitespace."""
125+
test_file_path = os.path.join(self.temp_dir.name, "empty_lines.txt")
126+
127+
# Content with empty lines and whitespace-only lines
128+
content_with_empty_lines = "Line 1\n\n \t \nLine 2\n"
129+
130+
# Expected content after whitespace stripping
131+
# The whitespace-only line should become an empty line
132+
expected_content = "Line 1\n\n\nLine 2"
133+
134+
async with self.create_client_session() as session:
135+
# Initialize project to get chat_id
136+
init_result_text = await self.call_tool_assert_success(
137+
session,
138+
"codemcp",
139+
{
140+
"subtool": "InitProject",
141+
"path": self.temp_dir.name,
142+
"user_prompt": "Test initialization for empty lines test",
143+
"subject_line": "test: initialize for empty lines test",
144+
"reuse_head_chat_id": False,
145+
},
146+
)
147+
148+
# Extract chat_id from the init result
149+
chat_id = self.extract_chat_id_from_text(init_result_text)
150+
151+
# Call the WriteFile tool with content containing empty lines
152+
result_text = await self.call_tool_assert_success(
153+
session,
154+
"codemcp",
155+
{
156+
"subtool": "WriteFile",
157+
"path": test_file_path,
158+
"content": content_with_empty_lines,
159+
"description": "Create file with empty lines",
160+
"chat_id": chat_id,
161+
},
162+
)
163+
164+
# Verify the success message
165+
self.assertIn("Successfully wrote to", result_text)
166+
167+
# Verify the file was created with empty lines preserved
168+
with open(test_file_path) as f:
169+
file_content = f.read()
170+
171+
self.assertEqual(file_content, expected_content)
172+
173+
174+
class OutOfProcessTrailingWhitespaceTest(TrailingWhitespaceTest):
175+
in_process = False
176+
177+
178+
if __name__ == "__main__":
179+
unittest.main()

0 commit comments

Comments
 (0)