Coverage for src/renaissance/utils/text_utils.py: 53%
98 statements
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-02 13:12 +0000
« prev ^ index » next coverage.py v7.16.1, created at 2026-10-02 13:12 +0000
1"""AI: Text manipulation utilities for shifting, indenting, and clipboard operations on source text."""
3import re
4import subprocess
5import sys
6import tempfile
7from pathlib import Path
9import pyperclip
12class TextUtils:
13 """AI: Text manipulation utilities for shifting, indenting, and clipboard operations on source text."""
15 __PRECEDING_SPACES_PATTERN = re.compile(r"([\t\s]*)")
17 @staticmethod
18 def shift_left(text: str, shift: int, start_line: int = 0) -> str:
19 """Shifts each line of the given text to the left by the specified number of spaces. Only spaces are shifted."""
20 if shift == 0:
21 return text
22 pattern = re.compile(r"\s{0," + str(shift) + "}(.*)")
23 lines = text.split("\n")
24 for idx, line in enumerate(lines[start_line:]):
25 lines[idx + start_line] = pattern.sub(r"\1", line)
26 return "\n".join(lines)
28 @staticmethod
29 def correct_indent(text: str, indent: int, depth: int = 0) -> str:
30 """Shifts each line of the given text to the left by the specified number of spaces. Only spaces are shifted."""
31 lines = text.split("\n")
32 for idx, line in enumerate(lines):
33 depth -= line.count("}")
34 stripped = re.sub(r"^\s*", "", line)
35 lines[idx] = " " * depth * indent + stripped if stripped else stripped
36 depth += line.count("{")
38 return "\n".join(lines)
40 @staticmethod
41 def strip_indent(text: str, start_line: int = 0) -> str:
42 """Shift the text left so the first line has no leading spaces.
44 All other lines are shifted left by the same amount.
45 """
46 matcher = TextUtils.__PRECEDING_SPACES_PATTERN.search(text)
47 if matcher:
48 spaces = matcher[1]
49 text = TextUtils.shift_left(text, len(spaces), start_line)
50 return text.strip()
52 @staticmethod
53 def shift_right(text: str, shift: int, start_line: int = 0) -> str:
54 """Shifts each line of the given text to the left by the specified number of spaces. Only spaces are shifted."""
55 if shift == 0:
56 return text
57 lines = text.split("\n")
58 spaces = " " * shift
59 for idx, line in enumerate(lines[start_line:]):
60 lines[idx + start_line] = spaces + line if line else line
61 return "\n".join(lines)
63 @staticmethod
64 def get_indent(content: bytes, offset: int) -> int:
65 """Calculate the indentation level of a line in a byte string.
67 Args:
68 content (bytes): The byte string containing the text.
69 offset (int): The position within the byte string to start calculating the indentation from.
71 Returns:
72 int: The number of leading whitespace characters (tabs or spaces) from the start of the line to the given offset.
74 """
75 indent = offset
76 while indent > 1:
77 if content[indent - 1] in b"\n\r":
78 break
79 indent -= 1
80 start_of_line = indent
81 while indent < offset:
82 if content[indent] not in b"\t ":
83 break
84 indent += 1
85 return indent - start_of_line
87 @staticmethod
88 def get_spaces_before(content: bytes, offset: int) -> int:
89 """Calculate the indentation level of a line in a byte string.
91 Args:
92 content (bytes): The byte string containing the text.
93 offset (int): The position within the byte string to start calculating the indentation from.
95 Returns:
96 int: The number of leading whitespace characters (tabs or spaces) from the start of the line to the given offset.
98 """
99 indent = offset - 1
100 while indent > 0:
101 if content[indent] not in b" \t":
102 break
103 indent -= 1
104 return offset - indent - 1
106 @staticmethod
107 def to_clipboard(text: str) -> None:
108 """AI: Copy the given text to the system clipboard."""
109 pyperclip.copy(text)
111 @staticmethod
112 def to_file(filename: str, text: str) -> None:
113 """Write the given text to a file with the specified filename."""
114 with Path(filename).open("w") as f:
115 f.write(text)
118def signature_to_id(signature: str) -> str:
119 """AI: Derive a short, filesystem/identifier-safe id from a node signature."""
120 text = signature.replace("\n", " ")
121 return re.sub(r"[^\w\s]", "", text)[:30] # Remove punctuation, limit length
124def snake_case(snippet: str) -> str:
125 """Convert a camelCase or PascalCase string to snake_case, preserving acronyms as single words.
127 Leaves a string already in snake_case unchanged.
128 """
129 snippet = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", snippet)
130 snippet = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", snippet)
131 return snippet.lower()
134def fix_indent(code_string: str) -> str | None:
135 """AI: Reformat code_string's indentation by round-tripping it through a temporary file and an external formatter."""
136 with tempfile.NamedTemporaryFile(suffix=".py", mode="w+", delete=False) as temp_file:
137 file_path = temp_file.name
138 temp_file.write(code_string)
140 try:
141 if not Path(file_path).is_file():
142 print(f"Error: {file_path} does not exist.")
143 return None
145 # Step 1: Run flake8 to show issues
146 print("Running flake8...")
147 subprocess.run([sys.executable, "-m", "flake8", file_path])
149 # Step 2: Auto-fix with autopep8
150 print("Auto-fixing with autopep8...")
151 subprocess.run(
152 [
153 sys.executable,
154 "-m",
155 "autopep8",
156 "--in-place",
157 "--aggressive",
158 "--aggressive",
159 file_path,
160 ],
161 )
163 # Step 3: Run flake8 again to verify
164 print("Re-running flake8 after fixes...")
165 subprocess.run([sys.executable, "-m", "flake8", file_path])
167 # Read the fixed code
168 with Path(file_path).open() as file:
169 fixed_code = file.read()
171 # black format
172 # return format_str(fixed_code, mode=FileMode())
173 return fixed_code
174 except Exception as e:
175 print(f"Error formatting code: {e}")
176 finally:
177 # Clean up the temporary file
178 if Path(file_path).exists():
179 Path(file_path).unlink()