pyscript-programming-language 1.14.0__py3-none-any.whl → 1.15.0.dev1__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.
Files changed (39) hide show
  1. pyscript/core/analyzer.py +31 -40
  2. pyscript/core/buffer.py +6 -10
  3. pyscript/core/cache.py +4 -3
  4. pyscript/core/checks.py +5 -21
  5. pyscript/core/constants.py +1 -1
  6. pyscript/core/editor/bases.py +1 -1
  7. pyscript/core/editor/gui.py +87 -6
  8. pyscript/core/editor/terminal.py +14 -1
  9. pyscript/core/highlight.py +23 -3
  10. pyscript/core/interpreter.py +38 -48
  11. pyscript/core/lexer.py +46 -44
  12. pyscript/core/mapping.py +3 -0
  13. pyscript/core/nodes.py +415 -203
  14. pyscript/core/parser.py +144 -208
  15. pyscript/core/pysbuiltins.py +4 -37
  16. pyscript/core/pystypes.py +50 -8
  17. pyscript/core/results.py +2 -2
  18. pyscript/core/shell.py +42 -63
  19. pyscript/core/utils/ansi.py +45 -23
  20. pyscript/core/utils/decorators.py +8 -8
  21. pyscript/core/utils/generic.py +1 -4
  22. pyscript/core/utils/module.py +6 -3
  23. pyscript/core/utils/string.py +3 -3
  24. pyscript/core/version.py +2 -2
  25. pyscript/lib/ast/ast_dump.py +20 -32
  26. pyscript/lib/ast/ast_literal_eval.py +5 -12
  27. pyscript/lib/ast/ast_unparse.py +10 -10
  28. pyscript/lib/ast/ast_walk.py +20 -26
  29. pyscript/lib/brainfuck.pys +7 -2
  30. pyscript/lib/dual/__init__.pys +17 -0
  31. pyscript/lib/dual/py_dual.py +246 -0
  32. pyscript/lib/getch/__init__.pys +6 -1
  33. pyscript/lib/getch/py_getch.py +92 -11
  34. pyscript/lib/history.pys +10 -8
  35. pyscript/lib/inspect.pys +1 -1
  36. {pyscript_programming_language-1.14.0.dist-info → pyscript_programming_language-1.15.0.dev1.dist-info}/METADATA +6 -5
  37. {pyscript_programming_language-1.14.0.dist-info → pyscript_programming_language-1.15.0.dev1.dist-info}/RECORD +39 -37
  38. {pyscript_programming_language-1.14.0.dist-info → pyscript_programming_language-1.15.0.dev1.dist-info}/WHEEL +0 -0
  39. {pyscript_programming_language-1.14.0.dist-info → pyscript_programming_language-1.15.0.dev1.dist-info}/top_level.txt +0 -0
pyscript/core/analyzer.py CHANGED
@@ -102,18 +102,17 @@ class PysAnalyzer(Pys):
102
102
 
103
103
  keyword_names = set()
104
104
 
105
- for element in node.arguments:
105
+ for argument in node.arguments:
106
106
 
107
- if element.__class__ is tuple:
108
- token, value = element
107
+ if argument.__class__ is tuple:
108
+ token, value = argument
109
109
  name = token.value
110
110
  if name in keyword_names:
111
111
  self.throw(f"duplicate argument {name!r} in call definition", token.position)
112
112
  return
113
113
  keyword_names.add(name)
114
-
115
114
  else:
116
- value = element
115
+ value = argument
117
116
 
118
117
  self.visit(value)
119
118
  if self.error:
@@ -163,7 +162,7 @@ class PysAnalyzer(Pys):
163
162
  self.visit_declaration_from_AssignmentNode(node.target, f"cannot {operator} literal", operator)
164
163
 
165
164
  def visit_StatementsNode(self, node: PysStatementsNode) -> None:
166
- for element in node.body:
165
+ for element in node.elements:
167
166
  self.visit(element)
168
167
  if self.error:
169
168
  return
@@ -179,7 +178,7 @@ class PysAnalyzer(Pys):
179
178
  self.visit(node.value)
180
179
 
181
180
  def visit_IfNode(self, node: PysIfNode) -> None:
182
- for condition, body in node.cases_body:
181
+ for condition, body in node.cases:
183
182
  self.visit(condition)
184
183
  if self.error:
185
184
  return
@@ -188,8 +187,8 @@ class PysAnalyzer(Pys):
188
187
  if self.error:
189
188
  return
190
189
 
191
- if node.else_body:
192
- self.visit(node.else_body)
190
+ if node.else_:
191
+ self.visit(node.else_)
193
192
 
194
193
  def visit_SwitchNode(self, node: PysSwitchNode) -> None:
195
194
  self.visit(node.target)
@@ -198,8 +197,8 @@ class PysAnalyzer(Pys):
198
197
 
199
198
  self.in_switch += 1
200
199
 
201
- for condition, body in node.case_cases:
202
- self.visit(condition)
200
+ for case, body in node.cases:
201
+ self.visit(case)
203
202
  if self.error:
204
203
  return
205
204
 
@@ -207,8 +206,8 @@ class PysAnalyzer(Pys):
207
206
  if self.error:
208
207
  return
209
208
 
210
- if node.default_body:
211
- self.visit(node.default_body)
209
+ if node.default:
210
+ self.visit(node.default)
212
211
  if self.error:
213
212
  return
214
213
 
@@ -220,8 +219,8 @@ class PysAnalyzer(Pys):
220
219
  if self.error:
221
220
  return
222
221
 
223
- for condition, value in node.cases:
224
- self.visit(condition)
222
+ for case, value in node.cases:
223
+ self.visit(case)
225
224
  if self.error:
226
225
  return
227
226
 
@@ -237,18 +236,18 @@ class PysAnalyzer(Pys):
237
236
  if self.error:
238
237
  return
239
238
 
240
- for _, body in node.catch_cases:
239
+ for _, body in node.catches:
241
240
  self.visit(body)
242
241
  if self.error:
243
242
  return
244
243
 
245
- if node.else_body:
246
- self.visit(node.else_body)
244
+ if node.else_:
245
+ self.visit(node.else_)
247
246
  if self.error:
248
247
  return
249
248
 
250
- if node.finally_body:
251
- self.visit(node.finally_body)
249
+ if node.finally_:
250
+ self.visit(node.finally_)
252
251
 
253
252
  def visit_WithNode(self, node: PysWithNode) -> None:
254
253
  for context, _ in node.contexts:
@@ -284,8 +283,8 @@ class PysAnalyzer(Pys):
284
283
 
285
284
  self.in_loop -= 1
286
285
 
287
- if node.else_body:
288
- self.visit(node.else_body)
286
+ if node.else_:
287
+ self.visit(node.else_)
289
288
 
290
289
  def visit_WhileNode(self, node: PysWhileNode) -> None:
291
290
  self.visit(node.condition)
@@ -300,8 +299,8 @@ class PysAnalyzer(Pys):
300
299
 
301
300
  self.in_loop -= 1
302
301
 
303
- if node.else_body:
304
- self.visit(node.else_body)
302
+ if node.else_:
303
+ self.visit(node.else_)
305
304
 
306
305
  def visit_DoWhileNode(self, node: PysDoWhileNode) -> None:
307
306
  self.in_loop += 1
@@ -316,11 +315,10 @@ class PysAnalyzer(Pys):
316
315
  if self.error:
317
316
  return
318
317
 
319
- if node.else_body:
320
- self.visit(node.else_body)
318
+ if node.else_:
319
+ self.visit(node.else_)
321
320
 
322
321
  def visit_RepeatNode(self, node: PysRepeatNode) -> None:
323
-
324
322
  if node.body:
325
323
  self.in_loop += 1
326
324
 
@@ -334,8 +332,8 @@ class PysAnalyzer(Pys):
334
332
  if self.error:
335
333
  return
336
334
 
337
- if node.else_body:
338
- self.visit(node.else_body)
335
+ if node.else_:
336
+ self.visit(node.else_)
339
337
 
340
338
  def visit_ClassNode(self, node: PysClassNode) -> None:
341
339
  for decorator in node.decorators:
@@ -378,9 +376,9 @@ class PysAnalyzer(Pys):
378
376
 
379
377
  parameter_names = set()
380
378
 
381
- for element in node.parameters:
382
- is_keyword = element.__class__ is tuple
383
- token = element[0] if is_keyword else element
379
+ for parameter in node.parameters:
380
+ is_keyword = parameter.__class__ is tuple
381
+ token = parameter[0] if is_keyword else parameter
384
382
  name = token.value
385
383
 
386
384
  if name in parameter_names:
@@ -389,7 +387,7 @@ class PysAnalyzer(Pys):
389
387
  parameter_names.add(name)
390
388
 
391
389
  if is_keyword:
392
- self.visit(element[1])
390
+ self.visit(parameter[1])
393
391
  if self.error:
394
392
  return
395
393
 
@@ -464,10 +462,6 @@ class PysAnalyzer(Pys):
464
462
  if self.error:
465
463
  return
466
464
 
467
- elif type is PysKeywordNode:
468
- self.throw(f"cannot delete {target.name.value}", target.position)
469
- return
470
-
471
465
  elif type is PysDebugNode:
472
466
  self.throw("cannot delete to __debug__", node.position)
473
467
  return
@@ -537,9 +531,6 @@ class PysAnalyzer(Pys):
537
531
  if self.error:
538
532
  return
539
533
 
540
- elif type is PysKeywordNode:
541
- self.throw(f"cannot {operator_name} to {node.name.value}", node.position)
542
-
543
534
  elif type is PysDebugNode:
544
535
  self.throw(f"cannot {operator_name} to __debug__", node.position)
545
536
 
pyscript/core/buffer.py CHANGED
@@ -5,7 +5,7 @@ from .utils.string import normstr
5
5
 
6
6
  from io import IOBase
7
7
  from types import BuiltinMethodType
8
- from typing import Iterable, Optional, Union
8
+ from typing import Iterable, Optional
9
9
 
10
10
  @immutable
11
11
  class PysBuffer(Pys):
@@ -17,27 +17,23 @@ class PysFileBuffer(PysBuffer):
17
17
 
18
18
  def __new__(
19
19
  cls,
20
- text: Union[str, bytes, bytearray, Iterable, BuiltinMethodType, IOBase, 'PysFileBuffer'],
20
+ text: 'str | bytes | bytearray | Iterable | BuiltinMethodType | IOBase | PysFileBuffer',
21
21
  name: Optional[str | bytes] = None
22
22
  ) -> 'PysFileBuffer':
23
23
 
24
24
  if isinstance(text, PysFileBuffer):
25
25
  return text
26
-
27
26
  elif isinstance(text, IOBase):
28
27
  name = normstr(getattr(text, 'name', '<io>') if name is None else name)
29
28
  text = normstr(text)
30
-
31
29
  else:
32
30
  name = '<string>' if name is None else normstr(name)
33
31
  text = normstr(text)
34
32
 
35
- instance = super().__new__(cls)
36
-
37
- setimuattr(instance, 'text', text)
38
- setimuattr(instance, 'name', name)
39
-
40
- return instance
33
+ self = super().__new__(cls)
34
+ setimuattr(self, 'text', text)
35
+ setimuattr(self, 'name', name)
36
+ return self
41
37
 
42
38
  def __repr__(self) -> str:
43
39
  return f'<FileBuffer from {self.name!r}>'
pyscript/core/cache.py CHANGED
@@ -18,6 +18,9 @@ pys_sys = ModuleType(
18
18
  "Python sys module."
19
19
  )
20
20
 
21
+ def intern_object(object: Any) -> Any:
22
+ return pys_sys.interns.setdefault((type(object), object), object)
23
+
21
24
  pys_sys.__running_shell__ = False
22
25
  pys_sys.__running_breakpoint__ = False
23
26
  pys_sys.builtin_module_names = tuple(
@@ -33,6 +36,7 @@ pys_sys.singletons = {}
33
36
  pys_sys.argv = ['']
34
37
  pys_sys.encoding = 'utf-8'
35
38
  pys_sys.flags = DEFAULT
39
+ pys_sys.intern_object = intern_object
36
40
  pys_sys.displayhook = print_display
37
41
  pys_sys.excepthook = print_traceback
38
42
  pys_sys.clearhook = clear_shell
@@ -58,9 +62,6 @@ for name in (
58
62
  if hasattr(sys, name):
59
63
  setattr(pys_sys, name, getattr(sys, name))
60
64
 
61
- def intern_object(object: Any) -> Any:
62
- return pys_sys.interns.setdefault((type(object), object), object)
63
-
64
65
  # sentinel
65
66
  @singleton
66
67
  @inheritable
pyscript/core/checks.py CHANGED
@@ -4,10 +4,9 @@ from .nodes import *
4
4
  from .token import TOKENS
5
5
 
6
6
  is_expression = frozenset([
7
- PysNumberNode, PysStringNode, PysKeywordNode, PysDebugNode, PysIdentifierNode, PysDictionaryNode, PysSetNode,
8
- PysListNode, PysTupleNode, PysAttributeNode, PysSubscriptNode, PysCallNode, PysChainOperatorNode,
9
- PysTernaryOperatorNode, PysBinaryOperatorNode, PysUnaryOperatorNode, PysIncrementalNode, PysMatchNode,
10
- PysFunctionNode, PysEllipsisNode
7
+ PysConstantNode, PysDebugNode, PysIdentifierNode, PysDictionaryNode, PysSetNode, PysListNode, PysTupleNode,
8
+ PysAttributeNode, PysSubscriptNode, PysCallNode, PysChainOperatorNode, PysTernaryOperatorNode,
9
+ PysBinaryOperatorNode, PysUnaryOperatorNode, PysIncrementalNode, PysMatchNode, PysFunctionNode, PysEllipsisNode
11
10
  ]).__contains__
12
11
 
13
12
  is_statement = frozenset([
@@ -16,31 +15,16 @@ is_statement = frozenset([
16
15
  PysAssertNode, PysDeleteNode, PysContinueNode, PysBreakNode
17
16
  ]).__contains__
18
17
 
19
- is_sequence = frozenset([
20
- PysSetNode, PysListNode, PysTupleNode
21
- ]).__contains__
22
-
23
- is_literal = frozenset([
24
- PysNumberNode, PysStringNode
25
- ]).__contains__
26
-
18
+ is_sequence = frozenset([PysSetNode, PysListNode, PysTupleNode]).__contains__
27
19
  is_keyword = frozenset(KEYWORDS).__contains__
28
20
  is_constant_keyword = frozenset(CONSTANT_KEYWORDS).__contains__
29
-
30
- is_equal = frozenset([
31
- TOKENS['EQUAL'], TOKENS['EQUAL_COLON']
32
- ]).__contains__
21
+ is_equal = frozenset([TOKENS['EQUAL'], TOKENS['EQUAL_COLON']]).__contains__
33
22
 
34
23
  is_blacklist_python_builtin = frozenset([
35
24
  'IndentationError', 'TabError', 'breakpoint', 'compile', 'copyright', 'credits', 'dir', 'eval', 'exec', 'help',
36
25
  'globals', 'license', 'locals', 'vars'
37
26
  ]).__contains__
38
27
 
39
- is_python_extension = frozenset([
40
- '.cgi', '.fcgi', '.gyp', '.gypi', '.ipy', '.lmi', '.pxd', '.pxi', '.py', '.py3', '.pyc', '.pyd', '.pyde', '.pyi',
41
- '.pyo', '.pyp', '.pyproj', '.pyt', '.pyw', '.pyx', '.pyz', '.rpy', '.spec', '.tac', '.wsgi', '.xpy'
42
- ]).__contains__
43
-
44
28
  is_left_bracket = frozenset(BRACKETS_MAP.keys()).__contains__
45
29
  is_right_bracket = frozenset(BRACKETS_MAP.values()).__contains__
46
30
  is_bracket = frozenset(BRACKETS_MAP.keys() | BRACKETS_MAP.values()).__contains__
@@ -17,7 +17,7 @@ ENV_PYSCRIPT_NO_COLOR_PROMPT = 'PYSCRIPT_NO_COLOR_PROMPT'
17
17
  ENV_PYSCRIPT_MAXIMUM_TRACEBACK_LINE = 'PYSCRIPT_MAXIMUM_TRACEBACK_LINE'
18
18
  ENV_PYSCRIPT_CLASSIC_LINE_SHELL = 'PYSCRIPT_CLASSIC_LINE_SHELL'
19
19
  ENV_PYSCRIPT_HISTORY_PATH = 'PYSCRIPT_HISTORY_PATH'
20
- ENV_PYSCRIPT_MAXIMUM_HISTORY_LINE = 'PYSCRIPT_MAXIMUM_HISTORY_LINE'
20
+ ENV_PYSCRIPT_HISTORY_LIMIT = 'PYSCRIPT_HISTORY_LIMIT'
21
21
 
22
22
  KEYWORDS = (
23
23
  '__debug__', 'False', 'None', 'True', 'and', 'as', 'assert', 'break', 'case', 'catch', 'class', 'constructor',
@@ -15,8 +15,8 @@ class PysEditor(Pys):
15
15
  @typecheck
16
16
  def __init__(self, file: PysFileBuffer, colored: bool = True) -> None:
17
17
  self.file = file
18
- self.colored = bool(colored)
19
18
  self.basename = basename(self.file.name)
19
+ self.colored = bool(colored)
20
20
  self.used = False
21
21
  self.modified = False
22
22
 
@@ -6,8 +6,17 @@ from ..utils.generic import boundary
6
6
  from ..version import __version__
7
7
 
8
8
  try:
9
- from tkinter import Tk, Scrollbar, Text, messagebox
10
- from tkinter.font import Font
9
+ from tkinter import Tk, Scrollbar, Text, Canvas, messagebox
10
+ from tkinter.font import families, Font
11
+
12
+ MONO_FALLBACKS = (
13
+ 'Fira Code',
14
+ 'Cascadia Code', 'SF Mono', 'Ubuntu Mono', 'Liberation Mono',
15
+ 'Consolas', 'Menlo', 'DejaVu Sans Mono',
16
+ 'Courier New', 'Lucida Console', 'Monaco', 'Andale Mono',
17
+ 'Courier', 'Terminal',
18
+ 'monospace'
19
+ )
11
20
 
12
21
  class PysGUIEditor(PysEditor, Tk):
13
22
 
@@ -45,6 +54,7 @@ try:
45
54
  if self.text.edit_modified():
46
55
  self.modified = True
47
56
  update()
57
+ refresh_line_numbers()
48
58
  self.text.edit_modified(False)
49
59
 
50
60
  def on_change_font(value):
@@ -53,6 +63,7 @@ try:
53
63
  size = boundary(font.cget('size') + value, 1, 127)
54
64
  font.config(size=size)
55
65
  update_tab_size()
66
+ refresh_line_numbers()
56
67
  self.set_configuration('gui-size-font', size)
57
68
  return 'break'
58
69
  return wrapper
@@ -61,6 +72,7 @@ try:
61
72
  text = self.text
62
73
  wrap = text.cget('wrap') != 'char'
63
74
  text.configure(wrap='char' if wrap else 'none')
75
+ refresh_line_numbers()
64
76
  self.set_configuration('wrap', wrap)
65
77
  return 'break'
66
78
 
@@ -86,9 +98,64 @@ try:
86
98
  text.see('insert')
87
99
  return 'break'
88
100
 
101
+ def draw_line_numbers():
102
+ self.line_job = None
103
+ text = self.text
104
+ canvas = self.line_numbers
105
+ font = self.font
106
+
107
+ bold = (font.cget('family'), font.cget('size'), 'bold')
108
+ digits = len(text.index('end-1c').split('.')[0])
109
+ padding = font.measure(' ')
110
+ width = font.measure('0' * digits) + padding * 2
111
+
112
+ canvas.delete('all')
113
+ canvas.configure(width=width, bg='#1f1f1f')
114
+
115
+ current = text.index('insert').split('.')[0]
116
+ index = text.index('@0,0')
117
+ previous = None
118
+
119
+ while True:
120
+ info = text.dlineinfo(index)
121
+ if info is None:
122
+ break
123
+
124
+ number = index.split('.')[0]
125
+ if number == previous:
126
+ break
127
+
128
+ canvas.create_text(
129
+ width - padding, info[1],
130
+ anchor='ne',
131
+ text=number,
132
+ font=bold if number == current else font,
133
+ fill='#ffffff'
134
+ )
135
+
136
+ previous = number
137
+ index = text.index(f'{index} +1 lines')
138
+
139
+ def refresh_line_numbers(event=None):
140
+ if self.line_job is None:
141
+ self.line_job = self.after_idle(draw_line_numbers)
142
+
143
+ def on_scroll(first, last):
144
+ self.scrollbar.set(first, last)
145
+ refresh_line_numbers()
146
+
147
+ installed = frozenset(families())
148
+ for font_name in MONO_FALLBACKS:
149
+ if font_name in installed:
150
+ break
151
+ else:
152
+ font_name = 'monospace'
153
+
89
154
  self.lexer = PygmentsPyScriptLexer()
90
- self.font = Font(family='Consolas', size=boundary(self.get_configuration('gui-size-font', 10), 1, 127))
155
+ self.font = Font(family=font_name, size=boundary(self.get_configuration('gui-size-font', 10), 1, 127))
156
+ self.line_job = None
91
157
  self.scrollbar = Scrollbar(self)
158
+ self.line_numbers = Canvas(self, width=0, highlightthickness=0, bd=0)
92
159
  self.text = Text(
93
160
  self,
94
161
  undo=True,
@@ -98,7 +165,7 @@ try:
98
165
  bg='#000000',
99
166
  fg='#ffffff',
100
167
  insertbackground='white',
101
- yscrollcommand=self.scrollbar.set
168
+ yscrollcommand=on_scroll
102
169
  )
103
170
 
104
171
  update_tab_size()
@@ -106,6 +173,8 @@ try:
106
173
  self.scrollbar.pack_configure(side='right', fill='y')
107
174
  self.scrollbar.configure(command=self.text.yview)
108
175
 
176
+ self.line_numbers.pack_configure(side='left', fill='y')
177
+ self.text.pack_configure(side='left', expand=True, fill='both')
109
178
  self.text.pack_configure(side='left', expand=True, fill='both')
110
179
 
111
180
  self.text.insert('1.0', self.file.text)
@@ -115,6 +184,9 @@ try:
115
184
  self.text.bind('<Return>', on_enter)
116
185
  self.text.bind('<Prior>', on_page_up)
117
186
  self.text.bind('<Next>', on_page_down)
187
+ self.text.bind('<KeyRelease>', refresh_line_numbers)
188
+ self.text.bind('<ButtonRelease-1>', refresh_line_numbers)
189
+ self.text.bind('<Configure>', refresh_line_numbers)
118
190
 
119
191
  self.bind('<Configure>', on_configure)
120
192
  self.bind_all('<Control-S>', on_save)
@@ -145,14 +217,16 @@ try:
145
217
  self.wm_protocol('WM_DELETE_WINDOW', on_close)
146
218
 
147
219
  if PYGMENTS and colored:
220
+ self.update_job = None
221
+
148
222
  tag_configure = self.text.tag_configure
149
223
  for token, style in PygmentsPyScriptStyle.list_styles():
150
224
  color = style['color']
151
225
  if color:
152
226
  tag_configure(str(token), foreground=f'#{color}')
153
227
 
154
- def update():
155
- self.title(f'PyScript {__version__} - {self.basename}{"*" if self.modified else ""}')
228
+ def refresh_highlight():
229
+ self.update_job = None
156
230
 
157
231
  index = '1.0'
158
232
 
@@ -172,11 +246,18 @@ try:
172
246
  text_tag_add(to_string(type), index, end_index)
173
247
  index = end_index
174
248
 
249
+ def update():
250
+ self.title(f'PyScript {__version__} - {self.basename}{"*" if self.modified else ""}')
251
+ if self.update_job is not None:
252
+ self.after_cancel(self.update_job)
253
+ self.update_job = self.after(500, refresh_highlight)
254
+
175
255
  else:
176
256
  def update():
177
257
  self.title(f'PyScript {__version__} - {self.basename}{"*" if self.modified else ""}')
178
258
 
179
259
  update()
260
+ refresh_line_numbers()
180
261
 
181
262
  def run(self) -> None:
182
263
  PysEditor.run(self)
@@ -65,6 +65,13 @@ try:
65
65
  title = file
66
66
  return ANSI(f'\x1b[7m{title}{" " * max(0, columns - len(title))}\x1b[0m')
67
67
 
68
+ def create_line_prefix(line_number, wrap_count):
69
+ width = len(str(self.text.document.line_count))
70
+ number = str(line_number + 1) if wrap_count == 0 else ''
71
+ is_current = line_number == self.text.document.cursor_position_row
72
+ style = 'bg:#ffffff fg:#000000' + (' bold' if is_current else '')
73
+ return [(style, f' {number:>{width}} '), ('', ' ')]
74
+
68
75
  else:
69
76
  app_other_keyword.update({
70
77
  'color_depth': ColorDepth.DEPTH_1_BIT
@@ -85,10 +92,16 @@ try:
85
92
  title = file
86
93
  return title + ' ' * max(0, columns - len(title))
87
94
 
95
+ def create_line_prefix(line_number, wrap_count):
96
+ width = len(str(self.text.document.line_count))
97
+ number = str(line_number + 1) if wrap_count == 0 else ''
98
+ return f' {number:>{width}} | '
99
+
88
100
  self.text = TextArea(
89
101
  multiline=True,
90
102
  wrap_lines=on_wrap,
91
103
  input_processors=[TabsProcessor(tabstop=4)],
104
+ get_line_prefix=create_line_prefix,
92
105
  **text_other_keyword
93
106
  )
94
107
 
@@ -188,7 +201,7 @@ try:
188
201
  )
189
202
 
190
203
  self.text.text = self.file.text
191
- self.text.buffer.on_text_changed += on_change
204
+ self.text.buffer.on_text_changed.add_handler(on_change)
192
205
 
193
206
  Application.__init__(
194
207
  self,
@@ -9,10 +9,11 @@ from .pysbuiltins import pys_builtins
9
9
  from .token import TOKENS
10
10
  from .utils.ansi import acolor
11
11
  from .utils.decorators import typecheck
12
+ from .utils.generic import dfrozen
12
13
  from .utils.string import normstr
13
14
 
14
15
  from html import escape as html_escape
15
- from typing import Callable, Optional
16
+ from typing import Callable, Iterator, Optional
16
17
 
17
18
  BUILTIN_TYPES = frozenset(
18
19
  name
@@ -31,10 +32,11 @@ try:
31
32
  from pygments.lexer import RegexLexer, include, bygroups
32
33
  from pygments.style import Style
33
34
  from pygments.token import (
34
- Comment, Error, Escape, Generic, Keyword, Name, Number, Operator, Punctuation, String, Whitespace
35
+ _TokenType, Comment, Error, Escape, Generic, Keyword, Name, Number, Operator, Punctuation, String, Whitespace
35
36
  )
36
37
  from pygments.unistring import xid_start, xid_continue
37
38
 
39
+ _brackets_tokens = dfrozen({index: getattr(Punctuation, f'Brackets{index}') for index in range(3)})
38
40
  _set_constant_keywords = frozenset(CONSTANT_KEYWORDS)
39
41
  _keywords = '|'.join(KEYWORDS)
40
42
  _unicode_name = f'[{xid_start}][{xid_continue}]*'
@@ -112,6 +114,10 @@ try:
112
114
  Comment.Single: HIGHLIGHT_MAP['comment'],
113
115
  Error: HIGHLIGHT_MAP['invalid'],
114
116
  Generic.Prompt: HIGHLIGHT_MAP['keyword-other'],
117
+
118
+ Punctuation.Brackets0: HIGHLIGHT_MAP['brackets-0'],
119
+ Punctuation.Brackets1: HIGHLIGHT_MAP['brackets-1'],
120
+ Punctuation.Brackets2: HIGHLIGHT_MAP['brackets-2']
115
121
  }
116
122
 
117
123
  class PygmentsPyScriptLexer(Pys, RegexLexer):
@@ -324,6 +330,20 @@ try:
324
330
 
325
331
  }
326
332
 
333
+ def get_tokens_unprocessed(self, *args, **kwargs) -> Iterator[tuple[int, _TokenType, str]]:
334
+ brackets_stack = []
335
+
336
+ for index, token, value in super().get_tokens_unprocessed(*args, **kwargs):
337
+ if token is Punctuation and len(value) == 1 and is_bracket(ttype := ord(value)):
338
+ if is_left_bracket(ttype):
339
+ token = _brackets_tokens[len(brackets_stack) % 3]
340
+ brackets_stack.append(ttype)
341
+ elif not (brackets_stack and BRACKETS_MAP[brackets_stack.pop()] == ttype):
342
+ token = Error
343
+ else:
344
+ token = _brackets_tokens[len(brackets_stack) % 3]
345
+ yield index, token, value
346
+
327
347
  class PygmentsPyScriptShellLexer(PygmentsPyScriptLexer):
328
348
 
329
349
  """
@@ -521,8 +541,8 @@ def pys_highlight(
521
541
  type_format = 'comment'
522
542
 
523
543
  elif is_bracket(ttype):
524
- type_format = f'brackets-{len(brackets_stack) % max_bracket_level}'
525
544
  if is_left_bracket(ttype):
545
+ type_format = f'brackets-{len(brackets_stack) % max_bracket_level}'
526
546
  brackets_stack.append(ttype)
527
547
  elif not (brackets_stack and BRACKETS_MAP[brackets_stack.pop()] == ttype):
528
548
  type_format = 'invalid'