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,811 @@
|
|
|
1
|
+
"""File explorer and fuzzy search functionality for SuperQode."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Iterable, List, Optional, Tuple
|
|
7
|
+
import fnmatch
|
|
8
|
+
from functools import lru_cache
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.table import Table
|
|
14
|
+
from rich.text import Text
|
|
15
|
+
from rich.prompt import Prompt, Confirm
|
|
16
|
+
except ImportError:
|
|
17
|
+
Console = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class PathFilter:
|
|
21
|
+
"""Filter paths based on gitignore patterns and common ignore rules."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, patterns: List[str]):
|
|
24
|
+
self.patterns = patterns
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_git_root(cls, path: Path) -> "PathFilter":
|
|
28
|
+
"""Create filter from .gitignore and common ignore patterns."""
|
|
29
|
+
patterns = []
|
|
30
|
+
|
|
31
|
+
# Read .gitignore if it exists
|
|
32
|
+
gitignore_path = path / ".gitignore"
|
|
33
|
+
if gitignore_path.exists():
|
|
34
|
+
try:
|
|
35
|
+
with open(gitignore_path, "r", encoding="utf-8") as f:
|
|
36
|
+
for line in f:
|
|
37
|
+
line = line.strip()
|
|
38
|
+
if line and not line.startswith("#"):
|
|
39
|
+
patterns.append(line)
|
|
40
|
+
except Exception:
|
|
41
|
+
pass # Ignore gitignore read errors
|
|
42
|
+
|
|
43
|
+
# Add common ignore patterns
|
|
44
|
+
common_patterns = [
|
|
45
|
+
".git/",
|
|
46
|
+
".git",
|
|
47
|
+
"__pycache__/",
|
|
48
|
+
"*.pyc",
|
|
49
|
+
"*.pyo",
|
|
50
|
+
"*.pyd",
|
|
51
|
+
".Python",
|
|
52
|
+
"build/",
|
|
53
|
+
"dist/",
|
|
54
|
+
"*.egg-info/",
|
|
55
|
+
".DS_Store",
|
|
56
|
+
"node_modules/",
|
|
57
|
+
".env",
|
|
58
|
+
".env.local",
|
|
59
|
+
".env.*",
|
|
60
|
+
"*.log",
|
|
61
|
+
".vscode/",
|
|
62
|
+
".idea/",
|
|
63
|
+
".ruff_cache/",
|
|
64
|
+
"*.swp",
|
|
65
|
+
"*.swo",
|
|
66
|
+
"*~",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
patterns.extend(common_patterns)
|
|
70
|
+
return cls(patterns)
|
|
71
|
+
|
|
72
|
+
def match(self, path: Path) -> bool:
|
|
73
|
+
"""Check if path matches any ignore pattern."""
|
|
74
|
+
path_str = str(path)
|
|
75
|
+
|
|
76
|
+
# Normalize path separators
|
|
77
|
+
path_str = path_str.replace(os.sep, "/")
|
|
78
|
+
|
|
79
|
+
# Check if path matches any pattern
|
|
80
|
+
for pattern in self.patterns:
|
|
81
|
+
if self._matches_pattern(path_str, pattern):
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
# Also check the path name only (for files in current directory)
|
|
85
|
+
path_name = path.name
|
|
86
|
+
for pattern in self.patterns:
|
|
87
|
+
if self._matches_pattern(path_name, pattern):
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
def _matches_pattern(self, path_str: str, pattern: str) -> bool:
|
|
93
|
+
"""Check if path matches a single pattern."""
|
|
94
|
+
# Normalize pattern separators
|
|
95
|
+
pattern = pattern.replace(os.sep, "/")
|
|
96
|
+
|
|
97
|
+
# Handle directory patterns (ending with /)
|
|
98
|
+
if pattern.endswith("/"):
|
|
99
|
+
pattern = pattern[:-1]
|
|
100
|
+
# Match directory or any file/directory inside it
|
|
101
|
+
return path_str == pattern or path_str.startswith(pattern + "/") or path_str == pattern
|
|
102
|
+
|
|
103
|
+
# Handle wildcards
|
|
104
|
+
return fnmatch.fnmatch(path_str, pattern) or fnmatch.fnmatch(path_str, pattern + "/*")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class CodeExplorer:
|
|
108
|
+
"""Simple file explorer for SuperQode CLI."""
|
|
109
|
+
|
|
110
|
+
def __init__(self, root_path: Optional[Path] = None):
|
|
111
|
+
self.root_path = root_path or Path.cwd()
|
|
112
|
+
self.path_filter = PathFilter.from_git_root(self.root_path)
|
|
113
|
+
self.console = Console()
|
|
114
|
+
|
|
115
|
+
def explore_directory(self, path: Optional[Path] = None, max_depth: int = 3) -> str:
|
|
116
|
+
"""Generate a text-based directory tree."""
|
|
117
|
+
explore_path = path or self.root_path
|
|
118
|
+
|
|
119
|
+
if not explore_path.exists():
|
|
120
|
+
return f"Path does not exist: {explore_path}"
|
|
121
|
+
|
|
122
|
+
if not explore_path.is_dir():
|
|
123
|
+
return f"Not a directory: {explore_path}"
|
|
124
|
+
|
|
125
|
+
tree_lines = []
|
|
126
|
+
tree_lines.append(f"📁 {explore_path.name}/")
|
|
127
|
+
tree_lines.extend(self._build_tree(explore_path, max_depth=max_depth, prefix=""))
|
|
128
|
+
|
|
129
|
+
return "\n".join(tree_lines)
|
|
130
|
+
|
|
131
|
+
def _build_tree(
|
|
132
|
+
self, path: Path, max_depth: int = 3, prefix: str = "", current_depth: int = 0
|
|
133
|
+
) -> List[str]:
|
|
134
|
+
"""Recursively build directory tree."""
|
|
135
|
+
if current_depth >= max_depth:
|
|
136
|
+
return []
|
|
137
|
+
|
|
138
|
+
lines = []
|
|
139
|
+
try:
|
|
140
|
+
items = sorted(path.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))
|
|
141
|
+
filtered_items = []
|
|
142
|
+
|
|
143
|
+
for item in items:
|
|
144
|
+
# Filter out items that match ignore patterns
|
|
145
|
+
try:
|
|
146
|
+
rel_path = item.relative_to(self.root_path)
|
|
147
|
+
if not self.path_filter.match(rel_path) and not self.path_filter.match(item):
|
|
148
|
+
filtered_items.append(item)
|
|
149
|
+
except ValueError:
|
|
150
|
+
# Item is not relative to root, check directly
|
|
151
|
+
if not self.path_filter.match(item):
|
|
152
|
+
filtered_items.append(item)
|
|
153
|
+
|
|
154
|
+
for i, item in enumerate(filtered_items):
|
|
155
|
+
is_last = i == len(filtered_items) - 1
|
|
156
|
+
connector = "└── " if is_last else "├── "
|
|
157
|
+
|
|
158
|
+
# Add emoji based on type
|
|
159
|
+
if item.is_dir():
|
|
160
|
+
icon = "📁"
|
|
161
|
+
elif item.suffix.lower() in [".py", ".js", ".ts", ".java", ".cpp", ".c", ".h"]:
|
|
162
|
+
icon = "📄"
|
|
163
|
+
elif item.suffix.lower() in [".md", ".txt", ".rst"]:
|
|
164
|
+
icon = "📝"
|
|
165
|
+
elif item.suffix.lower() in [".json", ".yaml", ".yml", ".toml"]:
|
|
166
|
+
icon = "⚙️"
|
|
167
|
+
elif item.suffix.lower() in [".png", ".jpg", ".jpeg", ".gif", ".svg"]:
|
|
168
|
+
icon = "🖼️"
|
|
169
|
+
else:
|
|
170
|
+
icon = "📄"
|
|
171
|
+
|
|
172
|
+
lines.append(f"{prefix}{connector}{icon} {item.name}")
|
|
173
|
+
|
|
174
|
+
if item.is_dir() and current_depth < max_depth - 1:
|
|
175
|
+
next_prefix = prefix + (" " if is_last else "│ ")
|
|
176
|
+
lines.extend(self._build_tree(item, max_depth, next_prefix, current_depth + 1))
|
|
177
|
+
|
|
178
|
+
except PermissionError:
|
|
179
|
+
lines.append(f"{prefix}└── 🔒 Permission denied")
|
|
180
|
+
|
|
181
|
+
return lines
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class FuzzySearch:
|
|
185
|
+
"""Fuzzy search implementation for finding files and content."""
|
|
186
|
+
|
|
187
|
+
def __init__(self, case_sensitive: bool = False, cache_size: int = 1024):
|
|
188
|
+
self.case_sensitive = case_sensitive
|
|
189
|
+
self.cache = {}
|
|
190
|
+
self.cache_size = cache_size
|
|
191
|
+
|
|
192
|
+
def match(self, query: str, candidate: str) -> Tuple[float, List[int]]:
|
|
193
|
+
"""Match query against candidate with scoring.
|
|
194
|
+
|
|
195
|
+
Returns:
|
|
196
|
+
Tuple of (score, list of matching positions)
|
|
197
|
+
Score of 0 means no match
|
|
198
|
+
"""
|
|
199
|
+
cache_key = (query, candidate)
|
|
200
|
+
if cache_key in self.cache:
|
|
201
|
+
return self.cache[cache_key]
|
|
202
|
+
|
|
203
|
+
if not query:
|
|
204
|
+
return 0.0, []
|
|
205
|
+
|
|
206
|
+
# Normalize case
|
|
207
|
+
if not self.case_sensitive:
|
|
208
|
+
candidate = candidate.lower()
|
|
209
|
+
query = query.lower()
|
|
210
|
+
|
|
211
|
+
score, positions = self._calculate_match(query, candidate)
|
|
212
|
+
result = (score, positions)
|
|
213
|
+
|
|
214
|
+
# Simple LRU-style cache
|
|
215
|
+
if len(self.cache) >= self.cache_size:
|
|
216
|
+
# Remove a random item (simple cache eviction)
|
|
217
|
+
self.cache.pop(next(iter(self.cache)))
|
|
218
|
+
self.cache[cache_key] = result
|
|
219
|
+
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
def _calculate_match(self, query: str, candidate: str) -> Tuple[float, List[int]]:
|
|
223
|
+
"""Calculate fuzzy match score and positions."""
|
|
224
|
+
if not query or not candidate:
|
|
225
|
+
return 0.0, []
|
|
226
|
+
|
|
227
|
+
# Find all positions where query characters appear in order
|
|
228
|
+
positions = []
|
|
229
|
+
query_idx = 0
|
|
230
|
+
candidate_idx = 0
|
|
231
|
+
|
|
232
|
+
while query_idx < len(query) and candidate_idx < len(candidate):
|
|
233
|
+
if query[query_idx] == candidate[candidate_idx]:
|
|
234
|
+
positions.append(candidate_idx)
|
|
235
|
+
query_idx += 1
|
|
236
|
+
candidate_idx += 1
|
|
237
|
+
|
|
238
|
+
# Must match all characters in query
|
|
239
|
+
if query_idx < len(query):
|
|
240
|
+
return 0.0, []
|
|
241
|
+
|
|
242
|
+
# Calculate score based on match quality
|
|
243
|
+
if not positions:
|
|
244
|
+
return 0.0, []
|
|
245
|
+
|
|
246
|
+
score = len(positions) # Base score from number of matches
|
|
247
|
+
|
|
248
|
+
# Bonus for consecutive matches
|
|
249
|
+
consecutive_bonus = 0
|
|
250
|
+
for i in range(1, len(positions)):
|
|
251
|
+
if positions[i] == positions[i - 1] + 1:
|
|
252
|
+
consecutive_bonus += 2
|
|
253
|
+
score += consecutive_bonus
|
|
254
|
+
|
|
255
|
+
# Bonus for matches at word boundaries
|
|
256
|
+
word_boundary_bonus = 0
|
|
257
|
+
for pos in positions:
|
|
258
|
+
if pos == 0 or not candidate[pos - 1].isalnum():
|
|
259
|
+
word_boundary_bonus += 3
|
|
260
|
+
score += word_boundary_bonus
|
|
261
|
+
|
|
262
|
+
# Penalty for gaps between matches
|
|
263
|
+
gap_penalty = 0
|
|
264
|
+
for i in range(1, len(positions)):
|
|
265
|
+
gap = positions[i] - positions[i - 1] - 1
|
|
266
|
+
gap_penalty += gap * 0.1
|
|
267
|
+
score -= gap_penalty
|
|
268
|
+
|
|
269
|
+
return max(0, score), positions
|
|
270
|
+
|
|
271
|
+
@classmethod
|
|
272
|
+
@lru_cache(maxsize=1024)
|
|
273
|
+
def get_word_starts(cls, candidate: str) -> List[int]:
|
|
274
|
+
"""Get positions of word starts for better scoring."""
|
|
275
|
+
positions = [0] # Start of string
|
|
276
|
+
for i, char in enumerate(candidate):
|
|
277
|
+
if i > 0 and not candidate[i - 1].isalnum() and char.isalnum():
|
|
278
|
+
positions.append(i)
|
|
279
|
+
return positions
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class FuzzyFileSearch:
|
|
283
|
+
"""Fuzzy search specifically for files."""
|
|
284
|
+
|
|
285
|
+
def __init__(self, root_path: Optional[Path] = None):
|
|
286
|
+
self.root_path = root_path or Path.cwd()
|
|
287
|
+
self.path_filter = PathFilter.from_git_root(self.root_path)
|
|
288
|
+
self.fuzzy_search = FuzzySearch()
|
|
289
|
+
self._file_cache = None
|
|
290
|
+
self._cache_timestamp = 0
|
|
291
|
+
|
|
292
|
+
def search_files(self, query: str, max_results: int = 20) -> List[Tuple[Path, str, float]]:
|
|
293
|
+
"""Search for files using fuzzy matching.
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
List of (file_path, relative_path, score) tuples
|
|
297
|
+
"""
|
|
298
|
+
if not query.strip():
|
|
299
|
+
return []
|
|
300
|
+
|
|
301
|
+
# Get all project files
|
|
302
|
+
files = self._get_project_files()
|
|
303
|
+
|
|
304
|
+
results = []
|
|
305
|
+
for file_path in files:
|
|
306
|
+
try:
|
|
307
|
+
# Use relative path for searching
|
|
308
|
+
rel_path = file_path.relative_to(self.root_path)
|
|
309
|
+
rel_str = str(rel_path)
|
|
310
|
+
|
|
311
|
+
# Search in filename and full path
|
|
312
|
+
score1, _ = self.fuzzy_search.match(query, rel_path.name)
|
|
313
|
+
score2, _ = self.fuzzy_search.match(query, rel_str)
|
|
314
|
+
|
|
315
|
+
# Use the better score
|
|
316
|
+
score = max(score1, score2)
|
|
317
|
+
if score > 0:
|
|
318
|
+
results.append((file_path, str(rel_path), score))
|
|
319
|
+
|
|
320
|
+
except ValueError:
|
|
321
|
+
# File not relative to root
|
|
322
|
+
continue
|
|
323
|
+
|
|
324
|
+
# Sort by score (highest first)
|
|
325
|
+
results.sort(key=lambda x: x[2], reverse=True)
|
|
326
|
+
return results[:max_results]
|
|
327
|
+
|
|
328
|
+
def _get_project_files(self) -> List[Path]:
|
|
329
|
+
"""Get all files in project, respecting gitignore."""
|
|
330
|
+
import time
|
|
331
|
+
|
|
332
|
+
# Simple caching to avoid rescanning on every search
|
|
333
|
+
now = time.time()
|
|
334
|
+
if self._file_cache is not None and now - self._cache_timestamp < 30: # 30 second cache
|
|
335
|
+
return self._file_cache
|
|
336
|
+
|
|
337
|
+
files = []
|
|
338
|
+
try:
|
|
339
|
+
for root, dirs, files_in_dir in os.walk(self.root_path):
|
|
340
|
+
# Filter directories
|
|
341
|
+
dirs[:] = [
|
|
342
|
+
d
|
|
343
|
+
for d in dirs
|
|
344
|
+
if d not in [".git"] and not self.path_filter.match(Path(root) / d)
|
|
345
|
+
]
|
|
346
|
+
|
|
347
|
+
for file in files_in_dir:
|
|
348
|
+
file_path = Path(root) / file
|
|
349
|
+
if not self.path_filter.match(file_path):
|
|
350
|
+
files.append(file_path)
|
|
351
|
+
|
|
352
|
+
except Exception:
|
|
353
|
+
# Fallback to current directory only
|
|
354
|
+
try:
|
|
355
|
+
files = [
|
|
356
|
+
f
|
|
357
|
+
for f in self.root_path.iterdir()
|
|
358
|
+
if f.is_file() and not self.path_filter.match(f)
|
|
359
|
+
]
|
|
360
|
+
except Exception:
|
|
361
|
+
files = []
|
|
362
|
+
|
|
363
|
+
self._file_cache = files
|
|
364
|
+
self._cache_timestamp = now
|
|
365
|
+
return files
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def fuzzy_find_files(query: str, max_results: int = 20) -> List[Tuple[Path, str, float]]:
|
|
369
|
+
"""Convenience function for fuzzy file search."""
|
|
370
|
+
searcher = FuzzyFileSearch()
|
|
371
|
+
return searcher.search_files(query, max_results)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def show_fuzzy_search_results(query: str, results: List[Tuple[Path, str, float]]):
|
|
375
|
+
"""Display fuzzy search results in a nice format with git status."""
|
|
376
|
+
if not results:
|
|
377
|
+
console = Console()
|
|
378
|
+
console.print(f"[yellow]No files found matching '{query}'[/yellow]")
|
|
379
|
+
return
|
|
380
|
+
|
|
381
|
+
console = Console()
|
|
382
|
+
console.print(f"\n[bold green]🔍 Found {len(results)} files matching '{query}':[/bold green]\n")
|
|
383
|
+
|
|
384
|
+
# Initialize git status tracker
|
|
385
|
+
git_tracker = GitStatusTracker(Path.cwd())
|
|
386
|
+
|
|
387
|
+
table = Table(show_header=True, header_style="bold blue")
|
|
388
|
+
table.add_column("File", style="cyan", no_wrap=True)
|
|
389
|
+
table.add_column("Path", style="white")
|
|
390
|
+
table.add_column("Status", style="magenta", width=6)
|
|
391
|
+
table.add_column("Score", style="green", justify="right")
|
|
392
|
+
|
|
393
|
+
for file_path, rel_path, score in results:
|
|
394
|
+
# Add file type emoji
|
|
395
|
+
if file_path.suffix.lower() in [".py", ".js", ".ts", ".java", ".cpp", ".c", ".h"]:
|
|
396
|
+
icon = "📄"
|
|
397
|
+
elif file_path.suffix.lower() in [".md", ".txt", ".rst"]:
|
|
398
|
+
icon = "📝"
|
|
399
|
+
elif file_path.suffix.lower() in [".json", ".yaml", ".yml", ".toml"]:
|
|
400
|
+
icon = "⚙️"
|
|
401
|
+
elif file_path.suffix.lower() in [".png", ".jpg", ".jpeg", ".gif", ".svg"]:
|
|
402
|
+
icon = "🖼️"
|
|
403
|
+
else:
|
|
404
|
+
icon = "📄"
|
|
405
|
+
|
|
406
|
+
# Get git status
|
|
407
|
+
git_status = git_tracker.get_status_emoji(file_path)
|
|
408
|
+
if not git_status:
|
|
409
|
+
git_status = "✅" # Clean
|
|
410
|
+
|
|
411
|
+
table.add_row(f"{icon} {file_path.name}", rel_path, git_status, f"{score:.1f}")
|
|
412
|
+
|
|
413
|
+
console.print(table)
|
|
414
|
+
console.print(f"\n[dim]💡 Use ':open <path>' to open a file[/dim]")
|
|
415
|
+
console.print(f"[dim]🔴 Modified 🟢 Added 🟡 Untracked 🔵 Staged ✅ Clean[/dim]")
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class GitStatusTracker:
|
|
419
|
+
"""Track git status for files in a repository."""
|
|
420
|
+
|
|
421
|
+
def __init__(self, repo_path: Path):
|
|
422
|
+
self.repo_path = repo_path
|
|
423
|
+
self.status_cache = {}
|
|
424
|
+
self.last_update = 0
|
|
425
|
+
self.cache_duration = 10 # seconds
|
|
426
|
+
|
|
427
|
+
def get_status(self, file_path: Path) -> str:
|
|
428
|
+
"""Get git status for a file."""
|
|
429
|
+
import time
|
|
430
|
+
import subprocess
|
|
431
|
+
|
|
432
|
+
# Check if cache is fresh
|
|
433
|
+
now = time.time()
|
|
434
|
+
if now - self.last_update > self.cache_duration:
|
|
435
|
+
self._update_status()
|
|
436
|
+
|
|
437
|
+
# Get relative path for lookup
|
|
438
|
+
try:
|
|
439
|
+
rel_path = file_path.relative_to(self.repo_path)
|
|
440
|
+
return self.status_cache.get(str(rel_path), "")
|
|
441
|
+
except ValueError:
|
|
442
|
+
return ""
|
|
443
|
+
|
|
444
|
+
def _update_status(self) -> None:
|
|
445
|
+
"""Update git status cache."""
|
|
446
|
+
import time
|
|
447
|
+
import subprocess
|
|
448
|
+
|
|
449
|
+
self.status_cache = {}
|
|
450
|
+
self.last_update = time.time()
|
|
451
|
+
|
|
452
|
+
try:
|
|
453
|
+
# Run git status --porcelain for efficient status
|
|
454
|
+
result = subprocess.run(
|
|
455
|
+
["git", "status", "--porcelain"],
|
|
456
|
+
cwd=self.repo_path,
|
|
457
|
+
capture_output=True,
|
|
458
|
+
text=True,
|
|
459
|
+
timeout=5,
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
if result.returncode == 0:
|
|
463
|
+
for line in result.stdout.split("\n"):
|
|
464
|
+
if line.strip():
|
|
465
|
+
status = line[:2].strip()
|
|
466
|
+
file_path = line[3:].strip()
|
|
467
|
+
|
|
468
|
+
# Map git status to our status indicators
|
|
469
|
+
if status.startswith("M") or status.endswith("M"):
|
|
470
|
+
self.status_cache[file_path] = "modified"
|
|
471
|
+
elif status.startswith("A") or status == "A":
|
|
472
|
+
self.status_cache[file_path] = "added"
|
|
473
|
+
elif status.startswith("D") or status.endswith("D"):
|
|
474
|
+
self.status_cache[file_path] = "deleted"
|
|
475
|
+
elif status == "??":
|
|
476
|
+
self.status_cache[file_path] = "untracked"
|
|
477
|
+
elif status.startswith("R"):
|
|
478
|
+
self.status_cache[file_path] = "renamed"
|
|
479
|
+
|
|
480
|
+
except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError):
|
|
481
|
+
# Git not available or not a git repo
|
|
482
|
+
pass
|
|
483
|
+
|
|
484
|
+
def get_status_emoji(self, file_path: Path) -> str:
|
|
485
|
+
"""Get status emoji for display."""
|
|
486
|
+
status = self.get_status(file_path)
|
|
487
|
+
return {
|
|
488
|
+
"modified": "🔴",
|
|
489
|
+
"added": "🟢",
|
|
490
|
+
"deleted": "🔴",
|
|
491
|
+
"untracked": "🟡",
|
|
492
|
+
"renamed": "🔵",
|
|
493
|
+
"staged": "🔵",
|
|
494
|
+
}.get(status, "")
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
class RecentFiles:
|
|
498
|
+
"""Track recently accessed files."""
|
|
499
|
+
|
|
500
|
+
def __init__(self, max_files: int = 20):
|
|
501
|
+
self.max_files = max_files
|
|
502
|
+
self.recent_files = []
|
|
503
|
+
self._load_from_disk()
|
|
504
|
+
|
|
505
|
+
def add_file(self, file_path: Path) -> None:
|
|
506
|
+
"""Add a file to recent files list."""
|
|
507
|
+
file_str = str(file_path.resolve())
|
|
508
|
+
|
|
509
|
+
# Remove if already exists
|
|
510
|
+
if file_str in self.recent_files:
|
|
511
|
+
self.recent_files.remove(file_str)
|
|
512
|
+
|
|
513
|
+
# Add to beginning
|
|
514
|
+
self.recent_files.insert(0, file_str)
|
|
515
|
+
|
|
516
|
+
# Trim to max size
|
|
517
|
+
self.recent_files = self.recent_files[: self.max_files]
|
|
518
|
+
|
|
519
|
+
# Save to disk
|
|
520
|
+
self._save_to_disk()
|
|
521
|
+
|
|
522
|
+
def get_recent_files(self, limit: int = 10) -> List[Path]:
|
|
523
|
+
"""Get list of recent files."""
|
|
524
|
+
return [Path(f) for f in self.recent_files[:limit] if Path(f).exists()]
|
|
525
|
+
|
|
526
|
+
def _load_from_disk(self) -> None:
|
|
527
|
+
"""Load recent files from disk."""
|
|
528
|
+
try:
|
|
529
|
+
import json
|
|
530
|
+
|
|
531
|
+
config_dir = Path.home() / ".superqode"
|
|
532
|
+
config_dir.mkdir(exist_ok=True)
|
|
533
|
+
recent_file = config_dir / "recent_files.json"
|
|
534
|
+
|
|
535
|
+
if recent_file.exists():
|
|
536
|
+
with open(recent_file, "r") as f:
|
|
537
|
+
data = json.load(f)
|
|
538
|
+
self.recent_files = data.get("files", [])
|
|
539
|
+
except Exception:
|
|
540
|
+
self.recent_files = []
|
|
541
|
+
|
|
542
|
+
def _save_to_disk(self) -> None:
|
|
543
|
+
"""Save recent files to disk."""
|
|
544
|
+
try:
|
|
545
|
+
import json
|
|
546
|
+
|
|
547
|
+
config_dir = Path.home() / ".superqode"
|
|
548
|
+
config_dir.mkdir(exist_ok=True)
|
|
549
|
+
recent_file = config_dir / "recent_files.json"
|
|
550
|
+
|
|
551
|
+
with open(recent_file, "w") as f:
|
|
552
|
+
json.dump({"files": self.recent_files}, f, indent=2)
|
|
553
|
+
except Exception:
|
|
554
|
+
pass
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
class Bookmarks:
|
|
558
|
+
"""Manage bookmarked files."""
|
|
559
|
+
|
|
560
|
+
def __init__(self):
|
|
561
|
+
self.bookmarks = {}
|
|
562
|
+
self._load_from_disk()
|
|
563
|
+
|
|
564
|
+
def add_bookmark(self, file_path: Path, name: str = None) -> bool:
|
|
565
|
+
"""Add a file to bookmarks."""
|
|
566
|
+
if not file_path.exists():
|
|
567
|
+
return False
|
|
568
|
+
|
|
569
|
+
if name is None:
|
|
570
|
+
name = file_path.name
|
|
571
|
+
|
|
572
|
+
self.bookmarks[name] = str(file_path.resolve())
|
|
573
|
+
self._save_to_disk()
|
|
574
|
+
return True
|
|
575
|
+
|
|
576
|
+
def remove_bookmark(self, name: str) -> bool:
|
|
577
|
+
"""Remove a bookmark."""
|
|
578
|
+
if name in self.bookmarks:
|
|
579
|
+
del self.bookmarks[name]
|
|
580
|
+
self._save_to_disk()
|
|
581
|
+
return True
|
|
582
|
+
return False
|
|
583
|
+
|
|
584
|
+
def get_bookmarks(self):
|
|
585
|
+
"""Get all bookmarks as Path objects."""
|
|
586
|
+
result = {}
|
|
587
|
+
for name, path_str in self.bookmarks.items():
|
|
588
|
+
path = Path(path_str)
|
|
589
|
+
if path.exists():
|
|
590
|
+
result[name] = path
|
|
591
|
+
return result
|
|
592
|
+
|
|
593
|
+
def get_bookmark(self, name: str) -> Optional[Path]:
|
|
594
|
+
"""Get a specific bookmark."""
|
|
595
|
+
path_str = self.bookmarks.get(name)
|
|
596
|
+
if path_str:
|
|
597
|
+
path = Path(path_str)
|
|
598
|
+
return path if path.exists() else None
|
|
599
|
+
return None
|
|
600
|
+
|
|
601
|
+
def _load_from_disk(self) -> None:
|
|
602
|
+
"""Load bookmarks from disk."""
|
|
603
|
+
try:
|
|
604
|
+
import json
|
|
605
|
+
|
|
606
|
+
config_dir = Path.home() / ".superqode"
|
|
607
|
+
config_dir.mkdir(exist_ok=True)
|
|
608
|
+
bookmark_file = config_dir / "bookmarks.json"
|
|
609
|
+
|
|
610
|
+
if bookmark_file.exists():
|
|
611
|
+
with open(bookmark_file, "r") as f:
|
|
612
|
+
self.bookmarks = json.load(f)
|
|
613
|
+
except Exception:
|
|
614
|
+
self.bookmarks = {}
|
|
615
|
+
|
|
616
|
+
def _save_to_disk(self) -> None:
|
|
617
|
+
"""Save bookmarks to disk."""
|
|
618
|
+
try:
|
|
619
|
+
import json
|
|
620
|
+
|
|
621
|
+
config_dir = Path.home() / ".superqode"
|
|
622
|
+
config_dir.mkdir(exist_ok=True)
|
|
623
|
+
bookmark_file = config_dir / "bookmarks.json"
|
|
624
|
+
|
|
625
|
+
with open(bookmark_file, "w") as f:
|
|
626
|
+
json.dump(self.bookmarks, f, indent=2)
|
|
627
|
+
except Exception:
|
|
628
|
+
pass
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def show_file_content(file_path: Path) -> None:
|
|
632
|
+
"""Launch interactive file explorer."""
|
|
633
|
+
if not self.console:
|
|
634
|
+
print("Rich library not available for interactive explorer")
|
|
635
|
+
return
|
|
636
|
+
|
|
637
|
+
current_path = self.root_path
|
|
638
|
+
|
|
639
|
+
while True:
|
|
640
|
+
# Clear screen and show current directory
|
|
641
|
+
self.console.clear()
|
|
642
|
+
|
|
643
|
+
# Show current path
|
|
644
|
+
self.console.print(f"\n[bold blue]📁 File Explorer[/bold blue]")
|
|
645
|
+
self.console.print(f"[dim]Current: {current_path}[/dim]\n")
|
|
646
|
+
|
|
647
|
+
# Show directory tree
|
|
648
|
+
tree = self.explore_directory(current_path, max_depth=2)
|
|
649
|
+
self.console.print(tree)
|
|
650
|
+
|
|
651
|
+
# Show options
|
|
652
|
+
self.console.print("\n[bold cyan]Options:[/bold cyan]")
|
|
653
|
+
self.console.print(" [1] Open file/directory")
|
|
654
|
+
self.console.print(" [2] Search files")
|
|
655
|
+
self.console.print(" [3] Go up (..)")
|
|
656
|
+
self.console.print(" [4] Go to root")
|
|
657
|
+
self.console.print(" [q] Quit")
|
|
658
|
+
|
|
659
|
+
choice = Prompt.ask("\nChoose option", choices=["1", "2", "3", "4", "q"], default="q")
|
|
660
|
+
|
|
661
|
+
if choice == "q":
|
|
662
|
+
break
|
|
663
|
+
elif choice == "1":
|
|
664
|
+
name = Prompt.ask("Enter file/directory name")
|
|
665
|
+
target = current_path / name
|
|
666
|
+
if target.exists():
|
|
667
|
+
if target.is_dir():
|
|
668
|
+
current_path = target
|
|
669
|
+
else:
|
|
670
|
+
# Open file (for now just show path)
|
|
671
|
+
self.console.print(f"[green]Selected file: {target}[/green]")
|
|
672
|
+
input("Press Enter to continue...")
|
|
673
|
+
else:
|
|
674
|
+
self.console.print(f"[red]Not found: {target}[/red]")
|
|
675
|
+
input("Press Enter to continue...")
|
|
676
|
+
elif choice == "2":
|
|
677
|
+
query = Prompt.ask("Search query")
|
|
678
|
+
results = self.find_files(query, max_results=10)
|
|
679
|
+
if results:
|
|
680
|
+
self.console.print(f"\n[bold green]Found {len(results)} files:[/bold green]")
|
|
681
|
+
for i, (path, rel_path) in enumerate(results, 1):
|
|
682
|
+
self.console.print(f" {i}. {rel_path}")
|
|
683
|
+
else:
|
|
684
|
+
self.console.print("[yellow]No files found[/yellow]")
|
|
685
|
+
input("\nPress Enter to continue...")
|
|
686
|
+
elif choice == "3":
|
|
687
|
+
if current_path != self.root_path:
|
|
688
|
+
current_path = current_path.parent
|
|
689
|
+
elif choice == "4":
|
|
690
|
+
current_path = self.root_path
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def show_file_explorer():
|
|
694
|
+
"""Show file explorer interface."""
|
|
695
|
+
console = Console()
|
|
696
|
+
|
|
697
|
+
try:
|
|
698
|
+
explorer = CodeExplorer()
|
|
699
|
+
tree = explorer.explore_directory(max_depth=3)
|
|
700
|
+
|
|
701
|
+
panel = Panel.fit(
|
|
702
|
+
tree,
|
|
703
|
+
title="[bold blue]📁 Project Files[/bold blue]",
|
|
704
|
+
border_style="blue",
|
|
705
|
+
padding=(1, 2),
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
console.print(panel)
|
|
709
|
+
console.print("\n[dim]Use ':files interactive' for full explorer[/dim]")
|
|
710
|
+
|
|
711
|
+
except Exception as e:
|
|
712
|
+
console.print(f"[red]Error opening file explorer: {e}[/red]")
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def show_file_content(file_path: Path, preview_only: bool = False) -> None:
|
|
716
|
+
"""Show file content or open in editor with syntax highlighting."""
|
|
717
|
+
console = Console()
|
|
718
|
+
|
|
719
|
+
if not file_path.exists():
|
|
720
|
+
console.print(f"[red]File not found: {file_path}[/red]")
|
|
721
|
+
return
|
|
722
|
+
|
|
723
|
+
if file_path.is_dir():
|
|
724
|
+
console.print(f"[yellow]Cannot open directory: {file_path}[/yellow]")
|
|
725
|
+
console.print(f"[dim]Use ':files interactive' to browse directories[/dim]")
|
|
726
|
+
return
|
|
727
|
+
|
|
728
|
+
# Add to recent files
|
|
729
|
+
recent = RecentFiles()
|
|
730
|
+
recent.add_file(file_path)
|
|
731
|
+
|
|
732
|
+
try:
|
|
733
|
+
# Try to open in editor first (unless preview_only is True)
|
|
734
|
+
if not preview_only:
|
|
735
|
+
import subprocess
|
|
736
|
+
import shutil
|
|
737
|
+
|
|
738
|
+
editors = ["code", "cursor", "subl", "vim", "nano"]
|
|
739
|
+
editor = None
|
|
740
|
+
|
|
741
|
+
for ed in editors:
|
|
742
|
+
if shutil.which(ed):
|
|
743
|
+
editor = ed
|
|
744
|
+
break
|
|
745
|
+
|
|
746
|
+
if editor:
|
|
747
|
+
console.print(f"[green]Opening {file_path} in {editor}...[/green]")
|
|
748
|
+
subprocess.run([editor, str(file_path)], check=False)
|
|
749
|
+
return
|
|
750
|
+
|
|
751
|
+
# Fall back to showing content with syntax highlighting
|
|
752
|
+
console.print(f"[bold cyan]📄 {file_path.name}[/bold cyan]")
|
|
753
|
+
console.print(f"[dim]{file_path}[/dim]")
|
|
754
|
+
console.print()
|
|
755
|
+
|
|
756
|
+
try:
|
|
757
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
758
|
+
content = f.read()
|
|
759
|
+
|
|
760
|
+
# Syntax highlighting using pygments
|
|
761
|
+
try:
|
|
762
|
+
from pygments import highlight
|
|
763
|
+
from pygments.lexers import get_lexer_for_filename, TextLexer
|
|
764
|
+
from pygments.formatters import TerminalFormatter
|
|
765
|
+
|
|
766
|
+
try:
|
|
767
|
+
lexer = get_lexer_for_filename(file_path.name)
|
|
768
|
+
except Exception:
|
|
769
|
+
lexer = TextLexer()
|
|
770
|
+
|
|
771
|
+
formatter = TerminalFormatter()
|
|
772
|
+
highlighted = highlight(content, lexer, formatter)
|
|
773
|
+
|
|
774
|
+
# Truncate if too long
|
|
775
|
+
if len(content) > 2000:
|
|
776
|
+
lines = highlighted.split("\n")
|
|
777
|
+
truncated = "\n".join(lines[:50]) # Show first 50 lines
|
|
778
|
+
console.print(truncated)
|
|
779
|
+
console.print(
|
|
780
|
+
f"[dim]... ({len(lines) - 50} more lines, {len(content)} total chars)[/dim]"
|
|
781
|
+
)
|
|
782
|
+
else:
|
|
783
|
+
console.print(highlighted)
|
|
784
|
+
|
|
785
|
+
except ImportError:
|
|
786
|
+
# Fallback without syntax highlighting
|
|
787
|
+
if len(content) > 1000:
|
|
788
|
+
console.print(content[:1000] + "\n[dim]... (truncated)[/dim]")
|
|
789
|
+
else:
|
|
790
|
+
console.print(content)
|
|
791
|
+
|
|
792
|
+
except UnicodeDecodeError:
|
|
793
|
+
console.print("[dim][Binary file - cannot display content][/dim]")
|
|
794
|
+
except Exception as e:
|
|
795
|
+
console.print(f"[red]Error reading file: {e}[/red]")
|
|
796
|
+
|
|
797
|
+
except Exception as e:
|
|
798
|
+
console.print(f"[red]Error opening file: {e}[/red]")
|
|
799
|
+
|
|
800
|
+
|
|
801
|
+
def interactive_file_explorer():
|
|
802
|
+
"""Launch full interactive file explorer."""
|
|
803
|
+
console = Console()
|
|
804
|
+
|
|
805
|
+
try:
|
|
806
|
+
explorer = CodeExplorer()
|
|
807
|
+
explorer.interactive_explorer()
|
|
808
|
+
except KeyboardInterrupt:
|
|
809
|
+
console.print("\n[green]File explorer closed.[/green]")
|
|
810
|
+
except Exception as e:
|
|
811
|
+
console.print(f"[red]Error in file explorer: {e}[/red]")
|