superqode 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- superqode-0.1.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SuperQode Agent - Minimal, Transparent Agent Loop.
|
|
3
|
+
|
|
4
|
+
Design Philosophy:
|
|
5
|
+
- MINIMAL HARNESS: No heavy system prompts, no opinionated formatting
|
|
6
|
+
- TRANSPARENT: What you see is what the model gets
|
|
7
|
+
- FAIR TESTING: Compare models on equal footing
|
|
8
|
+
- SIMPLE LOOP: prompt → model → tools → repeat
|
|
9
|
+
|
|
10
|
+
This is NOT trying to be the best coding agent.
|
|
11
|
+
This IS trying to be the fairest way to test model coding capabilities.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from .loop import AgentLoop, AgentConfig
|
|
15
|
+
from .system_prompts import SystemPromptLevel, get_system_prompt
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AgentLoop",
|
|
19
|
+
"AgentConfig",
|
|
20
|
+
"SystemPromptLevel",
|
|
21
|
+
"get_system_prompt",
|
|
22
|
+
]
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Edit Strategies - Fallback matching for edit operations.
|
|
3
|
+
|
|
4
|
+
When exact string match fails, these strategies are tried in order to find
|
|
5
|
+
a suitable match (e.g., whitespace differences, indentation, line trimming).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Generator, Tuple
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
# Similarity thresholds for block anchor fallback matching
|
|
14
|
+
SINGLE_CANDIDATE_SIMILARITY_THRESHOLD = 0.0
|
|
15
|
+
MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD = 0.3
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _levenshtein(a: str, b: str) -> int:
|
|
19
|
+
"""Levenshtein distance between two strings."""
|
|
20
|
+
if not a or not b:
|
|
21
|
+
return max(len(a), len(b))
|
|
22
|
+
# Build matrix
|
|
23
|
+
matrix = [[0] * (len(b) + 1) for _ in range(len(a) + 1)]
|
|
24
|
+
for i in range(len(a) + 1):
|
|
25
|
+
matrix[i][0] = i
|
|
26
|
+
for j in range(len(b) + 1):
|
|
27
|
+
matrix[0][j] = j
|
|
28
|
+
for i in range(1, len(a) + 1):
|
|
29
|
+
for j in range(1, len(b) + 1):
|
|
30
|
+
cost = 0 if a[i - 1] == b[j - 1] else 1
|
|
31
|
+
matrix[i][j] = min(
|
|
32
|
+
matrix[i - 1][j] + 1,
|
|
33
|
+
matrix[i][j - 1] + 1,
|
|
34
|
+
matrix[i - 1][j - 1] + cost,
|
|
35
|
+
)
|
|
36
|
+
return matrix[len(a)][len(b)]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _simple_replacer(content: str, find: str) -> Generator[str, None, None]:
|
|
40
|
+
"""Exact match only."""
|
|
41
|
+
if find in content:
|
|
42
|
+
yield find
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _line_trimmed_replacer(content: str, find: str) -> Generator[str, None, None]:
|
|
46
|
+
"""Match when each line matches after trimming whitespace."""
|
|
47
|
+
original_lines = content.split("\n")
|
|
48
|
+
search_lines = find.split("\n")
|
|
49
|
+
if search_lines and search_lines[-1] == "":
|
|
50
|
+
search_lines.pop()
|
|
51
|
+
for i in range(len(original_lines) - len(search_lines) + 1):
|
|
52
|
+
matches = True
|
|
53
|
+
for j in range(len(search_lines)):
|
|
54
|
+
if original_lines[i + j].strip() != search_lines[j].strip():
|
|
55
|
+
matches = False
|
|
56
|
+
break
|
|
57
|
+
if matches:
|
|
58
|
+
match_start = sum(len(original_lines[k]) + 1 for k in range(i))
|
|
59
|
+
match_end = match_start
|
|
60
|
+
for k in range(len(search_lines)):
|
|
61
|
+
match_end += len(original_lines[i + k])
|
|
62
|
+
if k < len(search_lines) - 1:
|
|
63
|
+
match_end += 1
|
|
64
|
+
yield content[match_start:match_end]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _block_anchor_replacer(content: str, find: str) -> Generator[str, None, None]:
|
|
68
|
+
"""Match by first/last line anchors with Levenshtein similarity for middle lines."""
|
|
69
|
+
original_lines = content.split("\n")
|
|
70
|
+
search_lines = find.split("\n")
|
|
71
|
+
if len(search_lines) < 3:
|
|
72
|
+
return
|
|
73
|
+
if search_lines and search_lines[-1] == "":
|
|
74
|
+
search_lines.pop()
|
|
75
|
+
first_line_search = search_lines[0].strip()
|
|
76
|
+
last_line_search = search_lines[-1].strip()
|
|
77
|
+
search_block_size = len(search_lines)
|
|
78
|
+
candidates = []
|
|
79
|
+
for i in range(len(original_lines)):
|
|
80
|
+
if original_lines[i].strip() != first_line_search:
|
|
81
|
+
continue
|
|
82
|
+
for j in range(i + 2, len(original_lines)):
|
|
83
|
+
if original_lines[j].strip() == last_line_search:
|
|
84
|
+
candidates.append((i, j))
|
|
85
|
+
break
|
|
86
|
+
if not candidates:
|
|
87
|
+
return
|
|
88
|
+
if len(candidates) == 1:
|
|
89
|
+
start_line, end_line = candidates[0]
|
|
90
|
+
actual_block_size = end_line - start_line + 1
|
|
91
|
+
lines_to_check = min(search_block_size - 2, actual_block_size - 2)
|
|
92
|
+
similarity = 0.0
|
|
93
|
+
if lines_to_check > 0:
|
|
94
|
+
for j in range(1, min(search_block_size - 1, actual_block_size - 1)):
|
|
95
|
+
orig = original_lines[start_line + j].strip()
|
|
96
|
+
search = search_lines[j].strip()
|
|
97
|
+
max_len = max(len(orig), len(search))
|
|
98
|
+
if max_len == 0:
|
|
99
|
+
continue
|
|
100
|
+
dist = _levenshtein(orig, search)
|
|
101
|
+
similarity += (1 - dist / max_len) / lines_to_check
|
|
102
|
+
else:
|
|
103
|
+
similarity = 1.0
|
|
104
|
+
if similarity >= SINGLE_CANDIDATE_SIMILARITY_THRESHOLD:
|
|
105
|
+
match_start = sum(len(original_lines[k]) + 1 for k in range(start_line))
|
|
106
|
+
match_end = match_start
|
|
107
|
+
for k in range(start_line, end_line + 1):
|
|
108
|
+
match_end += len(original_lines[k])
|
|
109
|
+
if k < end_line:
|
|
110
|
+
match_end += 1
|
|
111
|
+
yield content[match_start:match_end]
|
|
112
|
+
return
|
|
113
|
+
best_match = None
|
|
114
|
+
max_similarity = -1.0
|
|
115
|
+
for start_line, end_line in candidates:
|
|
116
|
+
actual_block_size = end_line - start_line + 1
|
|
117
|
+
lines_to_check = min(search_block_size - 2, actual_block_size - 2)
|
|
118
|
+
similarity = 0.0
|
|
119
|
+
if lines_to_check > 0:
|
|
120
|
+
for j in range(1, min(search_block_size - 1, actual_block_size - 1)):
|
|
121
|
+
orig = original_lines[start_line + j].strip()
|
|
122
|
+
search = search_lines[j].strip()
|
|
123
|
+
max_len = max(len(orig), len(search))
|
|
124
|
+
if max_len == 0:
|
|
125
|
+
continue
|
|
126
|
+
dist = _levenshtein(orig, search)
|
|
127
|
+
similarity += 1 - dist / max_len
|
|
128
|
+
similarity /= lines_to_check
|
|
129
|
+
else:
|
|
130
|
+
similarity = 1.0
|
|
131
|
+
if similarity > max_similarity:
|
|
132
|
+
max_similarity = similarity
|
|
133
|
+
best_match = (start_line, end_line)
|
|
134
|
+
if max_similarity >= MULTIPLE_CANDIDATES_SIMILARITY_THRESHOLD and best_match:
|
|
135
|
+
start_line, end_line = best_match
|
|
136
|
+
match_start = sum(len(original_lines[k]) + 1 for k in range(start_line))
|
|
137
|
+
match_end = match_start
|
|
138
|
+
for k in range(start_line, end_line + 1):
|
|
139
|
+
match_end += len(original_lines[k])
|
|
140
|
+
if k < end_line:
|
|
141
|
+
match_end += 1
|
|
142
|
+
yield content[match_start:match_end]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _whitespace_normalized_replacer(content: str, find: str) -> Generator[str, None, None]:
|
|
146
|
+
"""Normalize all whitespace to single spaces for matching."""
|
|
147
|
+
|
|
148
|
+
def normalize(s: str) -> str:
|
|
149
|
+
return re.sub(r"\s+", " ", s).strip()
|
|
150
|
+
|
|
151
|
+
normalized_find = normalize(find)
|
|
152
|
+
lines = content.split("\n")
|
|
153
|
+
for i, line in enumerate(lines):
|
|
154
|
+
if normalize(line) == normalized_find:
|
|
155
|
+
yield line
|
|
156
|
+
else:
|
|
157
|
+
if normalized_find in normalize(line):
|
|
158
|
+
words = find.strip().split()
|
|
159
|
+
if words:
|
|
160
|
+
pattern = re.escape(words[0])
|
|
161
|
+
for w in words[1:]:
|
|
162
|
+
pattern += r"\s+" + re.escape(w)
|
|
163
|
+
m = re.search(pattern, line)
|
|
164
|
+
if m:
|
|
165
|
+
yield m.group(0)
|
|
166
|
+
find_lines = find.split("\n")
|
|
167
|
+
if len(find_lines) > 1:
|
|
168
|
+
for i in range(len(lines) - len(find_lines) + 1):
|
|
169
|
+
block = "\n".join(lines[i : i + len(find_lines)])
|
|
170
|
+
if normalize(block) == normalized_find:
|
|
171
|
+
yield block
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _indentation_flexible_replacer(content: str, find: str) -> Generator[str, None, None]:
|
|
175
|
+
"""Ignore indentation differences by removing minimum indent."""
|
|
176
|
+
|
|
177
|
+
def remove_indentation(text: str) -> str:
|
|
178
|
+
lines = text.split("\n")
|
|
179
|
+
non_empty = [line for line in lines if line.strip()]
|
|
180
|
+
if not non_empty:
|
|
181
|
+
return text
|
|
182
|
+
min_indent = min(
|
|
183
|
+
len(m.group(1)) if (m := re.match(r"^(\s*)", line)) else 0 for line in non_empty
|
|
184
|
+
)
|
|
185
|
+
return "\n".join(line if not line.strip() else line[min_indent:] for line in lines)
|
|
186
|
+
|
|
187
|
+
normalized_find = remove_indentation(find)
|
|
188
|
+
content_lines = content.split("\n")
|
|
189
|
+
find_lines = find.split("\n")
|
|
190
|
+
for i in range(len(content_lines) - len(find_lines) + 1):
|
|
191
|
+
block = "\n".join(content_lines[i : i + len(find_lines)])
|
|
192
|
+
if remove_indentation(block) == normalized_find:
|
|
193
|
+
yield block
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _escape_normalized_replacer(content: str, find: str) -> Generator[str, None, None]:
|
|
197
|
+
"""Unescape \\n, \\t, etc. in the find string for matching."""
|
|
198
|
+
|
|
199
|
+
def unescape(s: str) -> str:
|
|
200
|
+
return re.sub(
|
|
201
|
+
r"\\(n|t|r|'|\"|`|\\|\n|\$)",
|
|
202
|
+
lambda m: {
|
|
203
|
+
"n": "\n",
|
|
204
|
+
"t": "\t",
|
|
205
|
+
"r": "\r",
|
|
206
|
+
"'": "'",
|
|
207
|
+
'"': '"',
|
|
208
|
+
"`": "`",
|
|
209
|
+
"\\": "\\",
|
|
210
|
+
"\n": "\n",
|
|
211
|
+
"$": "$",
|
|
212
|
+
}.get(m.group(1), m.group(0)),
|
|
213
|
+
s,
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
unescaped = unescape(find)
|
|
217
|
+
if unescaped in content:
|
|
218
|
+
yield unescaped
|
|
219
|
+
lines = content.split("\n")
|
|
220
|
+
find_lines = unescape(find).split("\n")
|
|
221
|
+
for i in range(len(lines) - len(find_lines) + 1):
|
|
222
|
+
block = "\n".join(lines[i : i + len(find_lines)])
|
|
223
|
+
if unescape(block) == unescaped:
|
|
224
|
+
yield block
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _trimmed_boundary_replacer(content: str, find: str) -> Generator[str, None, None]:
|
|
228
|
+
"""Try matching with trimmed find (leading/trailing whitespace removed)."""
|
|
229
|
+
trimmed = find.strip()
|
|
230
|
+
if trimmed == find:
|
|
231
|
+
return
|
|
232
|
+
if trimmed in content:
|
|
233
|
+
yield trimmed
|
|
234
|
+
lines = content.split("\n")
|
|
235
|
+
find_lines = find.split("\n")
|
|
236
|
+
for i in range(len(lines) - len(find_lines) + 1):
|
|
237
|
+
block = "\n".join(lines[i : i + len(find_lines)])
|
|
238
|
+
if block.strip() == trimmed:
|
|
239
|
+
yield block
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _context_aware_replacer(content: str, find: str) -> Generator[str, None, None]:
|
|
243
|
+
"""Match by first/last line anchors; require ~50% of middle lines to match."""
|
|
244
|
+
find_lines = find.split("\n")
|
|
245
|
+
if len(find_lines) < 3:
|
|
246
|
+
return
|
|
247
|
+
if find_lines and find_lines[-1] == "":
|
|
248
|
+
find_lines.pop()
|
|
249
|
+
first_line = find_lines[0].strip()
|
|
250
|
+
last_line = find_lines[-1].strip()
|
|
251
|
+
content_lines = content.split("\n")
|
|
252
|
+
for i in range(len(content_lines)):
|
|
253
|
+
if content_lines[i].strip() != first_line:
|
|
254
|
+
continue
|
|
255
|
+
for j in range(i + 2, len(content_lines)):
|
|
256
|
+
if content_lines[j].strip() == last_line:
|
|
257
|
+
block_lines = content_lines[i : j + 1]
|
|
258
|
+
block = "\n".join(block_lines)
|
|
259
|
+
if len(block_lines) == len(find_lines):
|
|
260
|
+
matching = 0
|
|
261
|
+
total = 0
|
|
262
|
+
for k in range(1, len(block_lines) - 1):
|
|
263
|
+
bl = block_lines[k].strip()
|
|
264
|
+
fl = find_lines[k].strip()
|
|
265
|
+
if bl or fl:
|
|
266
|
+
total += 1
|
|
267
|
+
if bl == fl:
|
|
268
|
+
matching += 1
|
|
269
|
+
if total == 0 or matching / total >= 0.5:
|
|
270
|
+
yield block
|
|
271
|
+
return
|
|
272
|
+
break
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _multi_occurrence_replacer(content: str, find: str) -> Generator[str, None, None]:
|
|
276
|
+
"""Yield each exact occurrence (for replace_all handling)."""
|
|
277
|
+
start = 0
|
|
278
|
+
while True:
|
|
279
|
+
idx = content.find(find, start)
|
|
280
|
+
if idx == -1:
|
|
281
|
+
break
|
|
282
|
+
yield find
|
|
283
|
+
start = idx + len(find)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
REPLACERS = [
|
|
287
|
+
_simple_replacer,
|
|
288
|
+
_line_trimmed_replacer,
|
|
289
|
+
_block_anchor_replacer,
|
|
290
|
+
_whitespace_normalized_replacer,
|
|
291
|
+
_indentation_flexible_replacer,
|
|
292
|
+
_escape_normalized_replacer,
|
|
293
|
+
_trimmed_boundary_replacer,
|
|
294
|
+
_context_aware_replacer,
|
|
295
|
+
_multi_occurrence_replacer,
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def replace_with_strategies(
|
|
300
|
+
content: str, old_string: str, new_string: str, replace_all: bool = False
|
|
301
|
+
) -> Tuple[str, int]:
|
|
302
|
+
"""
|
|
303
|
+
Replace old_string with new_string in content, trying multiple matching
|
|
304
|
+
strategies when exact match fails.
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Tuple of (new_content, replaced_count).
|
|
308
|
+
|
|
309
|
+
Raises:
|
|
310
|
+
ValueError: if old_string == new_string
|
|
311
|
+
ValueError: if old_string not found with any strategy
|
|
312
|
+
ValueError: if multiple matches and not replace_all
|
|
313
|
+
"""
|
|
314
|
+
if old_string == new_string:
|
|
315
|
+
raise ValueError("old_string and new_string must be different")
|
|
316
|
+
for replacer in REPLACERS:
|
|
317
|
+
for search in replacer(content, old_string):
|
|
318
|
+
idx = content.find(search)
|
|
319
|
+
if idx == -1:
|
|
320
|
+
continue
|
|
321
|
+
if replace_all:
|
|
322
|
+
count = content.count(search)
|
|
323
|
+
new_content = content.replace(search, new_string)
|
|
324
|
+
return (new_content, count)
|
|
325
|
+
last_idx = content.rfind(search)
|
|
326
|
+
if idx != last_idx:
|
|
327
|
+
count = content.count(search)
|
|
328
|
+
raise ValueError(
|
|
329
|
+
f"Found {count} occurrences of old_string. Provide more surrounding "
|
|
330
|
+
"lines in old_string to identify the correct match, or use replace_all=true."
|
|
331
|
+
)
|
|
332
|
+
new_content = content[:idx] + new_string + content[idx + len(search) :]
|
|
333
|
+
return (new_content, 1)
|
|
334
|
+
raise ValueError("old_string not found in content")
|