xmyshell 0.2.4__tar.gz → 0.2.6__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.
- {xmyshell-0.2.4/xmyshell.egg-info → xmyshell-0.2.6}/PKG-INFO +3 -1
- {xmyshell-0.2.4 → xmyshell-0.2.6}/README.md +2 -0
- {xmyshell-0.2.4 → xmyshell-0.2.6}/pyproject.toml +1 -1
- {xmyshell-0.2.4 → xmyshell-0.2.6}/xmyshell/completer.py +13 -2
- {xmyshell-0.2.4 → xmyshell-0.2.6}/xmyshell/kernel.py +1 -1
- {xmyshell-0.2.4 → xmyshell-0.2.6}/xmyshell/keybindings.py +125 -27
- {xmyshell-0.2.4 → xmyshell-0.2.6}/xmyshell/main.py +3 -1
- {xmyshell-0.2.4 → xmyshell-0.2.6}/xmyshell/meta.py +2 -0
- {xmyshell-0.2.4 → xmyshell-0.2.6/xmyshell.egg-info}/PKG-INFO +3 -1
- {xmyshell-0.2.4 → xmyshell-0.2.6}/LICENSE +0 -0
- {xmyshell-0.2.4 → xmyshell-0.2.6}/setup.cfg +0 -0
- {xmyshell-0.2.4 → xmyshell-0.2.6}/xmyshell/__init__.py +0 -0
- {xmyshell-0.2.4 → xmyshell-0.2.6}/xmyshell/__main__.py +0 -0
- {xmyshell-0.2.4 → xmyshell-0.2.6}/xmyshell/data/commands.json +0 -0
- {xmyshell-0.2.4 → xmyshell-0.2.6}/xmyshell/environment.py +0 -0
- {xmyshell-0.2.4 → xmyshell-0.2.6}/xmyshell/init.py +0 -0
- {xmyshell-0.2.4 → xmyshell-0.2.6}/xmyshell/lexer.py +0 -0
- {xmyshell-0.2.4 → xmyshell-0.2.6}/xmyshell/themes.py +0 -0
- {xmyshell-0.2.4 → xmyshell-0.2.6}/xmyshell/update.py +0 -0
- {xmyshell-0.2.4 → xmyshell-0.2.6}/xmyshell/utils.py +0 -0
- {xmyshell-0.2.4 → xmyshell-0.2.6}/xmyshell.egg-info/SOURCES.txt +0 -0
- {xmyshell-0.2.4 → xmyshell-0.2.6}/xmyshell.egg-info/dependency_links.txt +0 -0
- {xmyshell-0.2.4 → xmyshell-0.2.6}/xmyshell.egg-info/entry_points.txt +0 -0
- {xmyshell-0.2.4 → xmyshell-0.2.6}/xmyshell.egg-info/requires.txt +0 -0
- {xmyshell-0.2.4 → xmyshell-0.2.6}/xmyshell.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xmyshell
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: XmyShell is a shell-like tool built on top of prompt_toolkit which provides a persistent Python runtime.
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -164,6 +164,8 @@ The path of config file.
|
|
|
164
164
|
- `↑↓` -> Navigate through command history.
|
|
165
165
|
- `→` -> Apply selected history entry to the prompt.
|
|
166
166
|
- `Ctrl+R` -> Reverse incremental searching (history).
|
|
167
|
+
- `Ctrl+Z` -> Undo.
|
|
168
|
+
- `Ctrl+Y` -> Redo.
|
|
167
169
|
- `Ctrl+D` -> Exit XmyShell.
|
|
168
170
|
- Auto bracket/parenthesis/quote pairing
|
|
169
171
|
|
|
@@ -154,6 +154,8 @@ The path of config file.
|
|
|
154
154
|
- `↑↓` -> Navigate through command history.
|
|
155
155
|
- `→` -> Apply selected history entry to the prompt.
|
|
156
156
|
- `Ctrl+R` -> Reverse incremental searching (history).
|
|
157
|
+
- `Ctrl+Z` -> Undo.
|
|
158
|
+
- `Ctrl+Y` -> Redo.
|
|
157
159
|
- `Ctrl+D` -> Exit XmyShell.
|
|
158
160
|
- Auto bracket/parenthesis/quote pairing
|
|
159
161
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from collections.abc import Generator, Iterable
|
|
1
2
|
import os
|
|
2
3
|
import sys
|
|
3
4
|
import re
|
|
@@ -6,7 +7,7 @@ import json
|
|
|
6
7
|
import keyword
|
|
7
8
|
import pkgutil
|
|
8
9
|
from string import whitespace
|
|
9
|
-
from prompt_toolkit.completion import Completer, Completion
|
|
10
|
+
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
|
|
10
11
|
from prompt_toolkit.document import Document
|
|
11
12
|
from .environment import namespace, aliases
|
|
12
13
|
from .utils import truncate
|
|
@@ -335,7 +336,7 @@ class ShellCompleter(Completer):
|
|
|
335
336
|
display_meta="alias",
|
|
336
337
|
)
|
|
337
338
|
|
|
338
|
-
def
|
|
339
|
+
def _get_completions(self, document: Document, complete_event) -> Generator[Completion, None, None]:
|
|
339
340
|
text = document.text_before_cursor
|
|
340
341
|
if "=>" in text: return
|
|
341
342
|
segments = re.split(r'\|>|\|', text)
|
|
@@ -417,5 +418,15 @@ class ShellCompleter(Completer):
|
|
|
417
418
|
yield from self._subcommand_completion(lstripped)
|
|
418
419
|
yield from self._path_completions(lstripped)
|
|
419
420
|
|
|
421
|
+
def get_completions(self, document: Document, complete_event: CompleteEvent) -> list[Completion]:
|
|
422
|
+
completions: list[Completion] = list(self._get_completions(document, complete_event))
|
|
423
|
+
if len(completions) == 1:
|
|
424
|
+
# Trailing space bypasses prompt_toolkit's `completion_does_nothing`
|
|
425
|
+
# filter (which hides completions identical to the replaced text) and
|
|
426
|
+
# doubles as the shell argument separator. Invisible characters would
|
|
427
|
+
# also bypass the filter, but would corrupt command parsing.
|
|
428
|
+
completions[0].text = completions[0].text + " "
|
|
429
|
+
return completions
|
|
430
|
+
|
|
420
431
|
def completer_init() -> None:
|
|
421
432
|
namespace["shell_completer"] = ShellCompleter()
|
|
@@ -58,7 +58,7 @@ def pyeval(cmd_line: str) -> str:
|
|
|
58
58
|
val, (str, bytes, bytearray, memoryview)
|
|
59
59
|
) and isinstance(val, (Sequence, Generator)):
|
|
60
60
|
val = shlex.join(str(ele) for ele in val)
|
|
61
|
-
result.append(str(val) if val is not None else '')
|
|
61
|
+
result.append(str(val).replace("\n", " ") if val is not None else '')
|
|
62
62
|
except Exception as e:
|
|
63
63
|
pywarning(f"An error occured when unfolding {expr!r}, using original string instead"
|
|
64
64
|
f"\n{type(e).__name__}: {e}")
|
|
@@ -1,22 +1,60 @@
|
|
|
1
|
+
import time
|
|
1
2
|
from prompt_toolkit.buffer import Buffer
|
|
2
3
|
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
|
|
3
4
|
from prompt_toolkit.selection import SelectionState
|
|
4
5
|
from prompt_toolkit.keys import Keys
|
|
5
6
|
from prompt_toolkit.key_binding.bindings.auto_suggest import load_auto_suggest_bindings
|
|
7
|
+
from dataclasses import dataclass
|
|
6
8
|
|
|
9
|
+
@dataclass
|
|
10
|
+
class BufferInfo:
|
|
11
|
+
text: str
|
|
12
|
+
cursor_pos: int
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def from_buffer(buffer: Buffer) -> "BufferInfo":
|
|
16
|
+
return BufferInfo(buffer.text, buffer.cursor_position)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
MAX_HISTORY_ENTRY = 100
|
|
20
|
+
UNDO_MERGE_TIMEOUT = 0.5
|
|
7
21
|
bindings = KeyBindings()
|
|
8
|
-
_auto_editing = False
|
|
22
|
+
_auto_editing: bool = False
|
|
23
|
+
_previous_text: str = ""
|
|
24
|
+
_last_info: BufferInfo = BufferInfo("", 0)
|
|
25
|
+
_bracket_stack: list[str] = []
|
|
26
|
+
_undo_list: list[BufferInfo] = []
|
|
27
|
+
_redo_list: list[BufferInfo] = []
|
|
28
|
+
_last_edit_time: float = 0
|
|
29
|
+
_last_edit_kind: str | None = None # "insert" / "delete" / "replace"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def on_text_changed(buffer: Buffer) -> None:
|
|
33
|
+
global _last_info
|
|
34
|
+
if not _auto_editing:
|
|
35
|
+
_redo_list.clear()
|
|
36
|
+
if not _should_merge(buffer):
|
|
37
|
+
_undo_list.append(_last_info)
|
|
38
|
+
_last_info = BufferInfo.from_buffer(buffer)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def clean_buffer() -> None:
|
|
42
|
+
global _auto_editing, _previous_text, _last_info, _last_edit_time, _last_edit_kind
|
|
43
|
+
_auto_editing = False
|
|
44
|
+
_previous_text = ""
|
|
45
|
+
_last_info = BufferInfo("", 0)
|
|
46
|
+
_bracket_stack.clear()
|
|
47
|
+
_undo_list.clear()
|
|
48
|
+
_redo_list.clear()
|
|
49
|
+
_last_edit_time = 0
|
|
50
|
+
_last_edit_kind = None
|
|
9
51
|
|
|
10
|
-
def buffer_get_stack(buffer) -> list[str]:
|
|
11
|
-
if not hasattr(buffer, '_bracket_stack'):
|
|
12
|
-
buffer._bracket_stack = []
|
|
13
|
-
return buffer._bracket_stack
|
|
14
52
|
|
|
15
|
-
def is_cursor_before_right(buffer, char) -> bool:
|
|
53
|
+
def is_cursor_before_right(buffer: Buffer, char) -> bool:
|
|
16
54
|
pos = buffer.cursor_position
|
|
17
55
|
return pos < len(buffer.text) and buffer.text[pos] == char
|
|
18
56
|
|
|
19
|
-
def should_completion(buffer) -> bool:
|
|
57
|
+
def should_completion(buffer: Buffer) -> bool:
|
|
20
58
|
pos = buffer.cursor_position
|
|
21
59
|
return pos >= len(buffer.text) or buffer.text[pos].isspace() or buffer.text[pos] in ")]}>\"\'"
|
|
22
60
|
|
|
@@ -40,8 +78,9 @@ def _(event: KeyPressEvent) -> None:
|
|
|
40
78
|
_auto_editing = True
|
|
41
79
|
buffer.insert_text('()')
|
|
42
80
|
buffer.cursor_left()
|
|
43
|
-
|
|
81
|
+
_bracket_stack.append('(')
|
|
44
82
|
_auto_editing = False
|
|
83
|
+
on_text_changed(buffer)
|
|
45
84
|
else:
|
|
46
85
|
buffer.insert_text('(')
|
|
47
86
|
|
|
@@ -65,8 +104,9 @@ def _(event: KeyPressEvent) -> None:
|
|
|
65
104
|
_auto_editing = True
|
|
66
105
|
buffer.insert_text('[]')
|
|
67
106
|
buffer.cursor_left()
|
|
68
|
-
|
|
107
|
+
_bracket_stack.append('[')
|
|
69
108
|
_auto_editing = False
|
|
109
|
+
on_text_changed(buffer)
|
|
70
110
|
else:
|
|
71
111
|
buffer.insert_text('[')
|
|
72
112
|
|
|
@@ -90,8 +130,9 @@ def _(event: KeyPressEvent) -> None:
|
|
|
90
130
|
_auto_editing = True
|
|
91
131
|
buffer.insert_text('{}')
|
|
92
132
|
buffer.cursor_left()
|
|
93
|
-
|
|
133
|
+
_bracket_stack.append('{')
|
|
94
134
|
_auto_editing = False
|
|
135
|
+
on_text_changed(buffer)
|
|
95
136
|
else:
|
|
96
137
|
buffer.insert_text('{')
|
|
97
138
|
|
|
@@ -99,7 +140,7 @@ def _(event: KeyPressEvent) -> None:
|
|
|
99
140
|
def _(event: KeyPressEvent) -> None:
|
|
100
141
|
global _auto_editing
|
|
101
142
|
buffer = event.current_buffer
|
|
102
|
-
stack =
|
|
143
|
+
stack = _bracket_stack
|
|
103
144
|
if stack and stack[-1] == '(' and is_cursor_before_right(buffer, ')'):
|
|
104
145
|
_auto_editing = True
|
|
105
146
|
buffer.cursor_right()
|
|
@@ -112,7 +153,7 @@ def _(event: KeyPressEvent) -> None:
|
|
|
112
153
|
def _(event: KeyPressEvent) -> None:
|
|
113
154
|
global _auto_editing
|
|
114
155
|
buffer = event.current_buffer
|
|
115
|
-
stack =
|
|
156
|
+
stack = _bracket_stack
|
|
116
157
|
if stack and stack[-1] == '[' and is_cursor_before_right(buffer, ']'):
|
|
117
158
|
_auto_editing = True
|
|
118
159
|
buffer.cursor_right()
|
|
@@ -125,7 +166,7 @@ def _(event: KeyPressEvent) -> None:
|
|
|
125
166
|
def _(event: KeyPressEvent) -> None:
|
|
126
167
|
global _auto_editing
|
|
127
168
|
buffer = event.current_buffer
|
|
128
|
-
stack =
|
|
169
|
+
stack = _bracket_stack
|
|
129
170
|
if stack and stack[-1] == '{' and is_cursor_before_right(buffer, '}'):
|
|
130
171
|
_auto_editing = True
|
|
131
172
|
buffer.cursor_right()
|
|
@@ -150,7 +191,7 @@ def _(event: KeyPressEvent) -> None:
|
|
|
150
191
|
)
|
|
151
192
|
buffer.selection_state.shift_mode = True
|
|
152
193
|
return
|
|
153
|
-
stack =
|
|
194
|
+
stack = _bracket_stack
|
|
154
195
|
if stack and stack[-1] == '"' and is_cursor_before_right(buffer, '"'):
|
|
155
196
|
_auto_editing = True
|
|
156
197
|
buffer.cursor_right()
|
|
@@ -171,6 +212,7 @@ def _(event: KeyPressEvent) -> None:
|
|
|
171
212
|
buffer.cursor_left()
|
|
172
213
|
stack.append('"')
|
|
173
214
|
_auto_editing = False
|
|
215
|
+
on_text_changed(buffer)
|
|
174
216
|
else:
|
|
175
217
|
buffer.insert_text('"')
|
|
176
218
|
|
|
@@ -190,7 +232,7 @@ def _(event: KeyPressEvent) -> None:
|
|
|
190
232
|
)
|
|
191
233
|
buffer.selection_state.shift_mode = True
|
|
192
234
|
return
|
|
193
|
-
stack =
|
|
235
|
+
stack = _bracket_stack
|
|
194
236
|
if stack and stack[-1] == "'" and is_cursor_before_right(buffer, "'"):
|
|
195
237
|
_auto_editing = True
|
|
196
238
|
buffer.cursor_right()
|
|
@@ -203,6 +245,7 @@ def _(event: KeyPressEvent) -> None:
|
|
|
203
245
|
buffer.cursor_left()
|
|
204
246
|
stack.append("'")
|
|
205
247
|
_auto_editing = False
|
|
248
|
+
on_text_changed(buffer)
|
|
206
249
|
else:
|
|
207
250
|
buffer.insert_text("'")
|
|
208
251
|
|
|
@@ -216,12 +259,11 @@ def _(event: KeyPressEvent) -> None:
|
|
|
216
259
|
if buffer.selection_state:
|
|
217
260
|
start = min(buffer.cursor_position, buffer.selection_state.original_cursor_position)
|
|
218
261
|
end = max(buffer.cursor_position, buffer.selection_state.original_cursor_position)
|
|
219
|
-
_auto_editing = True
|
|
220
262
|
buffer.text = buffer.text[:start] + buffer.text[end:]
|
|
221
263
|
buffer.cursor_position = start
|
|
222
264
|
buffer.selection_state = None
|
|
223
|
-
|
|
224
|
-
|
|
265
|
+
_bracket_stack.clear()
|
|
266
|
+
buffer.start_completion()
|
|
225
267
|
return
|
|
226
268
|
|
|
227
269
|
if pos > 0 and pos < len(text):
|
|
@@ -232,7 +274,7 @@ def _(event: KeyPressEvent) -> None:
|
|
|
232
274
|
(left == '{' and right == '}') or \
|
|
233
275
|
(left == '"' and right == '"') or \
|
|
234
276
|
(left == "'" and right == "'"):
|
|
235
|
-
stack =
|
|
277
|
+
stack = _bracket_stack
|
|
236
278
|
_auto_editing = True
|
|
237
279
|
if stack and stack[-1] == left:
|
|
238
280
|
stack.pop()
|
|
@@ -242,20 +284,76 @@ def _(event: KeyPressEvent) -> None:
|
|
|
242
284
|
return
|
|
243
285
|
|
|
244
286
|
buffer.delete_before_cursor()
|
|
287
|
+
buffer.start_completion()
|
|
245
288
|
|
|
246
289
|
def on_cursor_position_changed(buffer: Buffer) -> None:
|
|
247
|
-
global _auto_editing
|
|
290
|
+
global _auto_editing, _last_info, _previous_text
|
|
291
|
+
|
|
248
292
|
if _auto_editing:
|
|
249
|
-
|
|
293
|
+
_previous_text = buffer.text
|
|
250
294
|
return
|
|
251
295
|
|
|
252
|
-
|
|
253
|
-
buffer._previous_text = buffer.text # type: ignore
|
|
254
|
-
return
|
|
296
|
+
_last_info = BufferInfo.from_buffer(buffer)
|
|
255
297
|
|
|
256
|
-
if buffer.text !=
|
|
257
|
-
|
|
298
|
+
if buffer.text != _previous_text:
|
|
299
|
+
_previous_text = buffer.text
|
|
258
300
|
return
|
|
259
301
|
|
|
260
302
|
# Any cursor movement without editing will clear the stack
|
|
261
|
-
|
|
303
|
+
_bracket_stack.clear()
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _reset_undo_merge() -> None:
|
|
307
|
+
global _last_edit_time, _last_edit_kind
|
|
308
|
+
_last_edit_time = 0.0
|
|
309
|
+
_last_edit_kind = None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _should_merge(buffer: Buffer) -> bool:
|
|
313
|
+
global _last_edit_time, _last_edit_kind
|
|
314
|
+
|
|
315
|
+
old_text = _last_info.text
|
|
316
|
+
new_text = buffer.text
|
|
317
|
+
if len(new_text) > len(old_text):
|
|
318
|
+
kind = "insert"
|
|
319
|
+
elif len(new_text) < len(old_text):
|
|
320
|
+
kind = "delete"
|
|
321
|
+
else:
|
|
322
|
+
kind = "replace"
|
|
323
|
+
|
|
324
|
+
now = time.monotonic()
|
|
325
|
+
same_kind = (kind == _last_edit_kind)
|
|
326
|
+
in_time = (now - _last_edit_time) <= UNDO_MERGE_TIMEOUT
|
|
327
|
+
|
|
328
|
+
_last_edit_time = now
|
|
329
|
+
_last_edit_kind = kind
|
|
330
|
+
|
|
331
|
+
return same_kind and in_time
|
|
332
|
+
|
|
333
|
+
@bindings.add(Keys.ControlZ)
|
|
334
|
+
def _(event: KeyPressEvent) -> None:
|
|
335
|
+
global _auto_editing, _last_info
|
|
336
|
+
_auto_editing = True
|
|
337
|
+
buffer = event.current_buffer
|
|
338
|
+
if _undo_list:
|
|
339
|
+
_redo_list.append(BufferInfo.from_buffer(buffer))
|
|
340
|
+
info = _undo_list.pop()
|
|
341
|
+
buffer.text = info.text
|
|
342
|
+
buffer._set_cursor_position(info.cursor_pos)
|
|
343
|
+
_auto_editing = False
|
|
344
|
+
_last_info = BufferInfo.from_buffer(buffer)
|
|
345
|
+
_reset_undo_merge()
|
|
346
|
+
|
|
347
|
+
@bindings.add(Keys.ControlY)
|
|
348
|
+
def _(event: KeyPressEvent) -> None:
|
|
349
|
+
global _auto_editing, _last_info
|
|
350
|
+
_auto_editing = True
|
|
351
|
+
buffer = event.current_buffer
|
|
352
|
+
if _redo_list:
|
|
353
|
+
_undo_list.append(BufferInfo.from_buffer(buffer))
|
|
354
|
+
info = _redo_list.pop()
|
|
355
|
+
buffer.text = info.text
|
|
356
|
+
buffer._set_cursor_position(info.cursor_pos)
|
|
357
|
+
_auto_editing = False
|
|
358
|
+
_last_info = BufferInfo.from_buffer(buffer)
|
|
359
|
+
_reset_undo_merge()
|
|
@@ -8,7 +8,7 @@ from prompt_toolkit.history import FileHistory
|
|
|
8
8
|
from prompt_toolkit.auto_suggest import AutoSuggestFromHistory
|
|
9
9
|
from .meta import WELCOME_MESSAGE
|
|
10
10
|
from .environment import namespace
|
|
11
|
-
from .keybindings import bindings, on_cursor_position_changed
|
|
11
|
+
from .keybindings import bindings, on_cursor_position_changed, on_text_changed, clean_buffer
|
|
12
12
|
from .init import xmyshell_init
|
|
13
13
|
from .kernel import pyeval, xmyshell
|
|
14
14
|
from .lexer import XmyShellLexer
|
|
@@ -29,6 +29,7 @@ def xmyshell_main() -> None:
|
|
|
29
29
|
lexer=XmyShellLexer(),
|
|
30
30
|
)
|
|
31
31
|
session.default_buffer.on_cursor_position_changed += on_cursor_position_changed
|
|
32
|
+
session.default_buffer.on_text_changed += on_text_changed
|
|
32
33
|
|
|
33
34
|
try:
|
|
34
35
|
while True:
|
|
@@ -43,6 +44,7 @@ def xmyshell_main() -> None:
|
|
|
43
44
|
start_time = time_ns()
|
|
44
45
|
namespace["exit_code"] = xmyshell(cmd_line)
|
|
45
46
|
namespace["exec_duration"] = time_ns() - start_time
|
|
47
|
+
clean_buffer()
|
|
46
48
|
except KeyboardInterrupt:
|
|
47
49
|
print("Ctrl-C")
|
|
48
50
|
except EOFError:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xmyshell
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
4
4
|
Summary: XmyShell is a shell-like tool built on top of prompt_toolkit which provides a persistent Python runtime.
|
|
5
5
|
Requires-Python: >=3.12
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
@@ -164,6 +164,8 @@ The path of config file.
|
|
|
164
164
|
- `↑↓` -> Navigate through command history.
|
|
165
165
|
- `→` -> Apply selected history entry to the prompt.
|
|
166
166
|
- `Ctrl+R` -> Reverse incremental searching (history).
|
|
167
|
+
- `Ctrl+Z` -> Undo.
|
|
168
|
+
- `Ctrl+Y` -> Redo.
|
|
167
169
|
- `Ctrl+D` -> Exit XmyShell.
|
|
168
170
|
- Auto bracket/parenthesis/quote pairing
|
|
169
171
|
|
|
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
|