easyrip 4.13.2__tar.gz → 4.14.0__tar.gz
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.
- {easyrip-4.13.2 → easyrip-4.14.0}/PKG-INFO +1 -1
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/__main__.py +7 -1
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_command.py +13 -3
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_main.py +31 -3
- easyrip-4.14.0/easyrip/easyrip_prompt.py +232 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/global_val.py +1 -1
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip.egg-info/PKG-INFO +1 -1
- easyrip-4.13.2/easyrip/easyrip_prompt.py +0 -151
- {easyrip-4.13.2 → easyrip-4.14.0}/LICENSE +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/README.md +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/__init__.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_config/config.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_config/config_key.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_log.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_mlang/__init__.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_mlang/global_lang_val.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_mlang/lang_en.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_mlang/lang_tag_val.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_mlang/lang_zh_Hans_CN.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_mlang/translator.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_web/__init__.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_web/http_server.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/easyrip_web/third_party_api.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/ripper/media_info.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/ripper/param.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/ripper/ripper.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/ripper/sub_and_font/__init__.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/ripper/sub_and_font/ass.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/ripper/sub_and_font/font.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/ripper/sub_and_font/subset.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip/utils.py +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip.egg-info/SOURCES.txt +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip.egg-info/dependency_links.txt +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip.egg-info/entry_points.txt +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip.egg-info/requires.txt +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/easyrip.egg-info/top_level.txt +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/pyproject.toml +0 -0
- {easyrip-4.13.2 → easyrip-4.14.0}/setup.cfg +0 -0
|
@@ -23,7 +23,12 @@ from .easyrip_command import (
|
|
|
23
23
|
)
|
|
24
24
|
from .easyrip_config.config import Config_key, config
|
|
25
25
|
from .easyrip_main import Ripper, get_input_prompt, init, log, run_command
|
|
26
|
-
from .easyrip_prompt import
|
|
26
|
+
from .easyrip_prompt import (
|
|
27
|
+
ConfigFileHistory,
|
|
28
|
+
CustomPromptCompleter,
|
|
29
|
+
SmartPathCompleter,
|
|
30
|
+
easyrip_prompt,
|
|
31
|
+
)
|
|
27
32
|
from .global_val import C_D, C_Z
|
|
28
33
|
|
|
29
34
|
|
|
@@ -128,6 +133,7 @@ def run() -> NoReturn:
|
|
|
128
133
|
(
|
|
129
134
|
_ctv_to_nc(cmd_ctv_tuple),
|
|
130
135
|
OptCompleter(opt_tree=_ctv_to_nd(ct.value for ct in Opt_type)),
|
|
136
|
+
CustomPromptCompleter(),
|
|
131
137
|
)
|
|
132
138
|
),
|
|
133
139
|
history=prompt_history,
|
|
@@ -253,12 +253,22 @@ class Cmd_type(enum.Enum):
|
|
|
253
253
|
description=(
|
|
254
254
|
"history\n" # .
|
|
255
255
|
" Show prompt history\n"
|
|
256
|
-
"
|
|
257
|
-
" Delete history file"
|
|
256
|
+
"history_clear\n"
|
|
257
|
+
" Delete history file\n"
|
|
258
|
+
"add <name:string> <cmd:string>\n"
|
|
259
|
+
" Add a custom prompt\n"
|
|
260
|
+
" e.g. prompt add myprompt echo my prompt\n"
|
|
261
|
+
"del <name:string>\n"
|
|
262
|
+
" Delete a custom prompt"
|
|
263
|
+
"show\n"
|
|
264
|
+
" Show custom prompt"
|
|
258
265
|
),
|
|
259
266
|
childs=(
|
|
260
267
|
Cmd_type_val(("history",)),
|
|
261
|
-
Cmd_type_val(("
|
|
268
|
+
Cmd_type_val(("history_clear",)),
|
|
269
|
+
Cmd_type_val(("add",)),
|
|
270
|
+
Cmd_type_val(("del",)),
|
|
271
|
+
Cmd_type_val(("show",)),
|
|
262
272
|
),
|
|
263
273
|
)
|
|
264
274
|
translate = Cmd_type_val(
|
|
@@ -345,7 +345,7 @@ def run_ripper_list(
|
|
|
345
345
|
log.info("Execute shutdown in {}s", shutdown_sec)
|
|
346
346
|
if os.name == "nt":
|
|
347
347
|
_cmd = (
|
|
348
|
-
f'shutdown /s /t {shutdown_sec} /c "{gettext("{} run completed, shutdown in {}s", PROJECT_TITLE, shutdown_sec)}"',
|
|
348
|
+
f'shutdown /s /t {shutdown_sec} /d p:4:0 /c "{gettext("{} run completed, shutdown in {}s", PROJECT_TITLE, shutdown_sec)}"',
|
|
349
349
|
)
|
|
350
350
|
elif os.name == "posix":
|
|
351
351
|
_cmd = (f"shutdown -h +{shutdown_sec // 60}",)
|
|
@@ -775,8 +775,36 @@ def run_command(command: Iterable[str] | str) -> bool:
|
|
|
775
775
|
) as f:
|
|
776
776
|
for line in f.read().splitlines():
|
|
777
777
|
log.send(line, is_format=False)
|
|
778
|
-
|
|
779
|
-
|
|
778
|
+
|
|
779
|
+
case "history_clear":
|
|
780
|
+
easyrip_prompt.clear_history()
|
|
781
|
+
|
|
782
|
+
case "add":
|
|
783
|
+
_name = cmd_list[2]
|
|
784
|
+
_cmd = (
|
|
785
|
+
command.split(maxsplit=3)[-1]
|
|
786
|
+
if isinstance(command, str)
|
|
787
|
+
else " ".join(cmd_list[3:])
|
|
788
|
+
)
|
|
789
|
+
if " " in _name:
|
|
790
|
+
log.error("The name must not contain spaces")
|
|
791
|
+
return False
|
|
792
|
+
log.info("Add custom prompt {!r} = {!r}", _name, _cmd)
|
|
793
|
+
return easyrip_prompt.add_custom_prompt(_name, _cmd)
|
|
794
|
+
|
|
795
|
+
case "del":
|
|
796
|
+
_name = cmd_list[2]
|
|
797
|
+
log.info("Del custom prompt {!r}", _name)
|
|
798
|
+
return easyrip_prompt.del_custom_prompt(_name)
|
|
799
|
+
|
|
800
|
+
case "show":
|
|
801
|
+
if _custom_prompt := easyrip_prompt.get_custom_prompt():
|
|
802
|
+
log.send(
|
|
803
|
+
"\n".join(
|
|
804
|
+
f"{k!r} = {v!r}" for k, v in _custom_prompt.items()
|
|
805
|
+
),
|
|
806
|
+
is_format=False,
|
|
807
|
+
)
|
|
780
808
|
|
|
781
809
|
case Cmd_type.translate:
|
|
782
810
|
if not (_infix := cmd_list[1]):
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import tomllib
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
|
|
6
|
+
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
|
|
7
|
+
from prompt_toolkit.document import Document
|
|
8
|
+
from prompt_toolkit.formatted_text import StyleAndTextTuples
|
|
9
|
+
from prompt_toolkit.history import FileHistory
|
|
10
|
+
|
|
11
|
+
from .global_val import C_Z, get_CONFIG_DIR
|
|
12
|
+
from .utils import type_match
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class easyrip_prompt:
|
|
16
|
+
PROMPT_HISTORY_FILE = get_CONFIG_DIR() / "prompt_history.txt"
|
|
17
|
+
PROMPT_CUSTOM_FILE = get_CONFIG_DIR() / "prompt_custom.toml"
|
|
18
|
+
|
|
19
|
+
__prompt_custom_data: dict[str, str] | None = None
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def clear_history(cls) -> None:
|
|
23
|
+
cls.PROMPT_HISTORY_FILE.unlink(True)
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def get_custom_prompt(cls) -> dict[str, str]:
|
|
27
|
+
if cls.__prompt_custom_data is not None:
|
|
28
|
+
return cls.__prompt_custom_data
|
|
29
|
+
|
|
30
|
+
cls.PROMPT_CUSTOM_FILE.touch()
|
|
31
|
+
with cls.PROMPT_CUSTOM_FILE.open("rb") as f:
|
|
32
|
+
data = tomllib.load(f)
|
|
33
|
+
assert type_match(data, dict[str, str])
|
|
34
|
+
cls.__prompt_custom_data = data
|
|
35
|
+
return data
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def update_custom_prompt(cls, data: dict[str, str]) -> bool:
|
|
39
|
+
cls.PROMPT_CUSTOM_FILE.touch()
|
|
40
|
+
with cls.PROMPT_CUSTOM_FILE.open("wt", encoding="utf-8", newline="\n") as f:
|
|
41
|
+
f.writelines(f"{k!r} = {v!r}\n" for k, v in data.items())
|
|
42
|
+
cls.__prompt_custom_data = None
|
|
43
|
+
return True
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def add_custom_prompt(cls, name: str, cmd: str) -> bool:
|
|
47
|
+
data: dict[str, str] = cls.get_custom_prompt()
|
|
48
|
+
assert type_match(data, dict[str, str])
|
|
49
|
+
|
|
50
|
+
if name in data:
|
|
51
|
+
from .easyrip_log import log
|
|
52
|
+
|
|
53
|
+
log.error("The name {!r} is already in custom prompt", name)
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
data[name] = cmd
|
|
57
|
+
|
|
58
|
+
cls.update_custom_prompt(data)
|
|
59
|
+
|
|
60
|
+
return True
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def del_custom_prompt(cls, name: str) -> bool:
|
|
64
|
+
data: dict[str, str] = cls.get_custom_prompt()
|
|
65
|
+
|
|
66
|
+
pop_res: bool
|
|
67
|
+
if name in data:
|
|
68
|
+
data.pop(name)
|
|
69
|
+
pop_res = True
|
|
70
|
+
cls.update_custom_prompt(data)
|
|
71
|
+
else:
|
|
72
|
+
from .easyrip_log import log
|
|
73
|
+
|
|
74
|
+
log.warning("The name {!r} not in custom prompt", name)
|
|
75
|
+
pop_res = False
|
|
76
|
+
|
|
77
|
+
return pop_res
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ConfigFileHistory(FileHistory):
|
|
81
|
+
def store_string(self, string: str) -> None:
|
|
82
|
+
if not string.startswith(C_Z):
|
|
83
|
+
super().store_string(string)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _highlight_fuzzy_match(
|
|
87
|
+
suggestion: str,
|
|
88
|
+
user_input: str,
|
|
89
|
+
style_config: dict | None = None,
|
|
90
|
+
) -> StyleAndTextTuples:
|
|
91
|
+
"""
|
|
92
|
+
高亮显示模糊匹配结果
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
suggestion: 建议的完整字符串
|
|
96
|
+
user_input: 用户输入的匹配字符
|
|
97
|
+
style_config: 样式配置字典
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
包含样式信息的格式化文本
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
if style_config is None:
|
|
104
|
+
style_config = {
|
|
105
|
+
"match_char": "class:fuzzymatch.inside.character",
|
|
106
|
+
"match_section": "class:fuzzymatch.inside",
|
|
107
|
+
"non_match": "class:fuzzymatch.outside",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if not user_input:
|
|
111
|
+
# 用户没有输入,返回原始字符串
|
|
112
|
+
return [(style_config["non_match"], suggestion)]
|
|
113
|
+
|
|
114
|
+
# 找到最佳匹配位置
|
|
115
|
+
result = []
|
|
116
|
+
|
|
117
|
+
# 简化的模糊匹配算法
|
|
118
|
+
pattern = ".*?".join(map(re.escape, user_input))
|
|
119
|
+
regex = re.compile(pattern, re.IGNORECASE)
|
|
120
|
+
|
|
121
|
+
match = regex.search(suggestion)
|
|
122
|
+
if not match:
|
|
123
|
+
# 没有匹配,返回原始字符串
|
|
124
|
+
return [(style_config["non_match"], suggestion)]
|
|
125
|
+
|
|
126
|
+
start, end = match.span()
|
|
127
|
+
match_text = suggestion[start:end]
|
|
128
|
+
|
|
129
|
+
# 匹配段之前的文本
|
|
130
|
+
if start > 0:
|
|
131
|
+
result.append((style_config["non_match"], suggestion[:start]))
|
|
132
|
+
|
|
133
|
+
# 匹配段内部的字符
|
|
134
|
+
input_chars = list(user_input)
|
|
135
|
+
for char in match_text:
|
|
136
|
+
if input_chars and char.lower() == input_chars[0].lower():
|
|
137
|
+
result.append((style_config["match_char"], char))
|
|
138
|
+
input_chars.pop(0)
|
|
139
|
+
else:
|
|
140
|
+
result.append((style_config["match_section"], char))
|
|
141
|
+
|
|
142
|
+
# 匹配段之后的文本
|
|
143
|
+
if end < len(suggestion):
|
|
144
|
+
result.append((style_config["non_match"], suggestion[end:]))
|
|
145
|
+
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _fuzzy_filter_and_sort(names: list[str], match_str: str) -> list[str]:
|
|
150
|
+
"""模糊过滤和排序"""
|
|
151
|
+
if not match_str:
|
|
152
|
+
return sorted(names)
|
|
153
|
+
|
|
154
|
+
# 构建模糊匹配模式
|
|
155
|
+
pattern = ".*?".join(map(re.escape, match_str))
|
|
156
|
+
regex = re.compile(f"(?=({pattern}))", re.IGNORECASE)
|
|
157
|
+
|
|
158
|
+
matches = []
|
|
159
|
+
for filename in names:
|
|
160
|
+
regex_matches = list(regex.finditer(filename))
|
|
161
|
+
if regex_matches:
|
|
162
|
+
# 找到最佳匹配(最左、最短)
|
|
163
|
+
best = min(regex_matches, key=lambda m: (m.start(), len(m.group(1))))
|
|
164
|
+
matches.append((best.start(), len(best.group(1)), filename))
|
|
165
|
+
|
|
166
|
+
# 按匹配质量排序:先按匹配位置,再按匹配长度
|
|
167
|
+
matches.sort(key=lambda x: (x[0], x[1]))
|
|
168
|
+
return [item[2] for item in matches]
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class SmartPathCompleter(Completer):
|
|
172
|
+
def __init__(self) -> None:
|
|
173
|
+
pass
|
|
174
|
+
|
|
175
|
+
def get_completions(
|
|
176
|
+
self,
|
|
177
|
+
document: Document,
|
|
178
|
+
complete_event: CompleteEvent, # noqa: ARG002
|
|
179
|
+
) -> Iterable[Completion]:
|
|
180
|
+
text = document.text_before_cursor
|
|
181
|
+
input_path = text.strip("\"'")
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
directory = os.path.dirname(input_path) or "."
|
|
185
|
+
basename = os.path.basename(input_path)
|
|
186
|
+
|
|
187
|
+
filenames: list[str] = (
|
|
188
|
+
os.listdir(directory) if os.path.isdir(directory) else []
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
for filename in _fuzzy_filter_and_sort(filenames, basename):
|
|
192
|
+
full_name = (
|
|
193
|
+
filename if directory == "." else os.path.join(directory, filename)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if os.path.isdir(full_name):
|
|
197
|
+
filename += "/"
|
|
198
|
+
|
|
199
|
+
completion = full_name
|
|
200
|
+
|
|
201
|
+
if any(c in r""" !$%&()*:;<=>?[]^`{|}~""" for c in completion):
|
|
202
|
+
completion = f'"{completion}"'
|
|
203
|
+
|
|
204
|
+
yield Completion(
|
|
205
|
+
text=completion,
|
|
206
|
+
start_position=-len(text),
|
|
207
|
+
display=_highlight_fuzzy_match(filename, basename),
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
except OSError:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class CustomPromptCompleter(Completer):
|
|
215
|
+
def get_completions(
|
|
216
|
+
self,
|
|
217
|
+
document: Document,
|
|
218
|
+
complete_event: CompleteEvent, # noqa: ARG002
|
|
219
|
+
) -> Iterable[Completion]:
|
|
220
|
+
text = document.text_before_cursor
|
|
221
|
+
words = text.split()
|
|
222
|
+
|
|
223
|
+
custom_prompt = easyrip_prompt.get_custom_prompt()
|
|
224
|
+
for word in words[-1:]:
|
|
225
|
+
for name in _fuzzy_filter_and_sort(list(custom_prompt), word):
|
|
226
|
+
target_cmd = custom_prompt[name]
|
|
227
|
+
yield Completion(
|
|
228
|
+
text=target_cmd,
|
|
229
|
+
start_position=-len(word),
|
|
230
|
+
display=_highlight_fuzzy_match(name, word),
|
|
231
|
+
display_meta=target_cmd,
|
|
232
|
+
)
|
|
@@ -4,7 +4,7 @@ from functools import cache
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
6
|
PROJECT_NAME = "Easy Rip"
|
|
7
|
-
PROJECT_VERSION = "4.
|
|
7
|
+
PROJECT_VERSION = "4.14.0"
|
|
8
8
|
PROJECT_TITLE = f"{PROJECT_NAME} v{PROJECT_VERSION}"
|
|
9
9
|
PROJECT_URL = "https://github.com/op200/EasyRip"
|
|
10
10
|
PROJECT_RELEASE_API = "https://api.github.com/repos/op200/EasyRip/releases/latest"
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import re
|
|
3
|
-
from collections.abc import Iterable
|
|
4
|
-
|
|
5
|
-
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
|
|
6
|
-
from prompt_toolkit.document import Document
|
|
7
|
-
from prompt_toolkit.formatted_text import StyleAndTextTuples
|
|
8
|
-
from prompt_toolkit.history import FileHistory
|
|
9
|
-
|
|
10
|
-
from .global_val import C_Z, get_CONFIG_DIR
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class easyrip_prompt:
|
|
14
|
-
PROMPT_HISTORY_FILE = get_CONFIG_DIR() / "prompt_history.txt"
|
|
15
|
-
|
|
16
|
-
@classmethod
|
|
17
|
-
def clear(cls) -> None:
|
|
18
|
-
cls.PROMPT_HISTORY_FILE.unlink(True)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class ConfigFileHistory(FileHistory):
|
|
22
|
-
def store_string(self, string: str) -> None:
|
|
23
|
-
if not string.startswith(C_Z):
|
|
24
|
-
super().store_string(string)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
class SmartPathCompleter(Completer):
|
|
28
|
-
def __init__(self) -> None:
|
|
29
|
-
pass
|
|
30
|
-
|
|
31
|
-
def _highlight_fuzzy_match(
|
|
32
|
-
self,
|
|
33
|
-
suggestion: str,
|
|
34
|
-
user_input: str,
|
|
35
|
-
style_config: dict | None = None,
|
|
36
|
-
) -> StyleAndTextTuples:
|
|
37
|
-
"""
|
|
38
|
-
高亮显示模糊匹配结果
|
|
39
|
-
|
|
40
|
-
Args:
|
|
41
|
-
suggestion: 建议的完整字符串
|
|
42
|
-
user_input: 用户输入的匹配字符
|
|
43
|
-
style_config: 样式配置字典
|
|
44
|
-
|
|
45
|
-
Returns:
|
|
46
|
-
包含样式信息的格式化文本
|
|
47
|
-
|
|
48
|
-
"""
|
|
49
|
-
if style_config is None:
|
|
50
|
-
style_config = {
|
|
51
|
-
"match_char": "class:fuzzymatch.inside.character",
|
|
52
|
-
"match_section": "class:fuzzymatch.inside",
|
|
53
|
-
"non_match": "class:fuzzymatch.outside",
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if not user_input:
|
|
57
|
-
# 用户没有输入,返回原始字符串
|
|
58
|
-
return [(style_config["non_match"], suggestion)]
|
|
59
|
-
|
|
60
|
-
# 找到最佳匹配位置
|
|
61
|
-
result = []
|
|
62
|
-
|
|
63
|
-
# 简化的模糊匹配算法
|
|
64
|
-
pattern = ".*?".join(map(re.escape, user_input))
|
|
65
|
-
regex = re.compile(pattern, re.IGNORECASE)
|
|
66
|
-
|
|
67
|
-
match = regex.search(suggestion)
|
|
68
|
-
if not match:
|
|
69
|
-
# 没有匹配,返回原始字符串
|
|
70
|
-
return [(style_config["non_match"], suggestion)]
|
|
71
|
-
|
|
72
|
-
start, end = match.span()
|
|
73
|
-
match_text = suggestion[start:end]
|
|
74
|
-
|
|
75
|
-
# 匹配段之前的文本
|
|
76
|
-
if start > 0:
|
|
77
|
-
result.append((style_config["non_match"], suggestion[:start]))
|
|
78
|
-
|
|
79
|
-
# 匹配段内部的字符
|
|
80
|
-
input_chars = list(user_input)
|
|
81
|
-
for char in match_text:
|
|
82
|
-
if input_chars and char.lower() == input_chars[0].lower():
|
|
83
|
-
result.append((style_config["match_char"], char))
|
|
84
|
-
input_chars.pop(0)
|
|
85
|
-
else:
|
|
86
|
-
result.append((style_config["match_section"], char))
|
|
87
|
-
|
|
88
|
-
# 匹配段之后的文本
|
|
89
|
-
if end < len(suggestion):
|
|
90
|
-
result.append((style_config["non_match"], suggestion[end:]))
|
|
91
|
-
|
|
92
|
-
return result
|
|
93
|
-
|
|
94
|
-
def _fuzzy_filter_and_sort(self, filenames: list[str], match_str: str) -> list[str]:
|
|
95
|
-
"""模糊过滤和排序"""
|
|
96
|
-
if not match_str:
|
|
97
|
-
return sorted(filenames)
|
|
98
|
-
|
|
99
|
-
# 构建模糊匹配模式
|
|
100
|
-
pattern = ".*?".join(map(re.escape, match_str))
|
|
101
|
-
regex = re.compile(f"(?=({pattern}))", re.IGNORECASE)
|
|
102
|
-
|
|
103
|
-
matches = []
|
|
104
|
-
for filename in filenames:
|
|
105
|
-
regex_matches = list(regex.finditer(filename))
|
|
106
|
-
if regex_matches:
|
|
107
|
-
# 找到最佳匹配(最左、最短)
|
|
108
|
-
best = min(regex_matches, key=lambda m: (m.start(), len(m.group(1))))
|
|
109
|
-
matches.append((best.start(), len(best.group(1)), filename))
|
|
110
|
-
|
|
111
|
-
# 按匹配质量排序:先按匹配位置,再按匹配长度
|
|
112
|
-
matches.sort(key=lambda x: (x[0], x[1]))
|
|
113
|
-
return [item[2] for item in matches]
|
|
114
|
-
|
|
115
|
-
def get_completions(
|
|
116
|
-
self,
|
|
117
|
-
document: Document,
|
|
118
|
-
complete_event: CompleteEvent, # noqa: ARG002
|
|
119
|
-
) -> Iterable[Completion]:
|
|
120
|
-
text = document.text_before_cursor
|
|
121
|
-
input_path = text.strip("\"'")
|
|
122
|
-
|
|
123
|
-
try:
|
|
124
|
-
directory = os.path.dirname(input_path) or "."
|
|
125
|
-
basename = os.path.basename(input_path)
|
|
126
|
-
|
|
127
|
-
filenames: list[str] = (
|
|
128
|
-
os.listdir(directory) if os.path.isdir(directory) else []
|
|
129
|
-
)
|
|
130
|
-
|
|
131
|
-
for filename in self._fuzzy_filter_and_sort(filenames, basename):
|
|
132
|
-
full_name = (
|
|
133
|
-
filename if directory == "." else os.path.join(directory, filename)
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
if os.path.isdir(full_name):
|
|
137
|
-
filename += "/"
|
|
138
|
-
|
|
139
|
-
completion = full_name
|
|
140
|
-
|
|
141
|
-
if any(c in r""" !$%&()*:;<=>?[]^`{|}~""" for c in completion):
|
|
142
|
-
completion = f'"{completion}"'
|
|
143
|
-
|
|
144
|
-
yield Completion(
|
|
145
|
-
text=completion,
|
|
146
|
-
start_position=-len(text),
|
|
147
|
-
display=self._highlight_fuzzy_match(filename, basename),
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
except OSError:
|
|
151
|
-
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|