OILS / display / ui.py View on Github | oils.pub

649 lines, 339 significant
1# Copyright 2016 Andy Chu. All rights reserved.
2# Licensed under the Apache License, Version 2.0 (the "License");
3# you may not use this file except in compliance with the License.
4# You may obtain a copy of the License at
5#
6# http://www.apache.org/licenses/LICENSE-2.0
7"""
8ui.py - User interface constructs.
9"""
10from __future__ import print_function
11
12from _devbuild.gen.id_kind_asdl import Id, Id_t, Id_str
13from _devbuild.gen.syntax_asdl import (
14 Token,
15 SourceLine,
16 loc,
17 loc_e,
18 loc_t,
19 command_t,
20 command_str,
21 source,
22 source_e,
23)
24from _devbuild.gen.value_asdl import value, value_e, value_t
25from asdl import format as fmt
26from data_lang import j8_lite
27from display import pp_value
28from display import pretty
29from frontend import lexer
30from frontend import location
31from mycpp import mylib
32from mycpp.mylib import print_stderr, tagswitch, log
33import libc
34
35from typing import List, Tuple, Optional, Any, cast, TYPE_CHECKING
36if TYPE_CHECKING:
37 from _devbuild.gen import arg_types
38 from core import error
39 from core.error import _ErrorWithLocation
40
41_ = log
42
43
44def ValType(val):
45 # type: (value_t) -> str
46 """For displaying type errors in the UI."""
47
48 # TODO: consolidate these functions
49 return pp_value.ValType(val)
50
51
52def CommandType(cmd):
53 # type: (command_t) -> str
54 """For displaying commands in the UI."""
55
56 # Displays 'Simple', 'BraceGroup', etc.
57 return command_str(cmd.tag(), dot=False)
58
59
60def PrettyId(id_):
61 # type: (Id_t) -> str
62 """For displaying type errors in the UI."""
63
64 # Displays 'Id.BoolUnary_v' for now
65 return Id_str(id_)
66
67
68def PrettyToken(tok):
69 # type: (Token) -> str
70 """Returns a readable token value for the user.
71
72 For syntax errors.
73 """
74 if tok.id == Id.Eof_Real:
75 return 'EOF'
76
77 val = tok.line.content[tok.col:tok.col + tok.length]
78 # TODO: Print length 0 as 'EOF'?
79 return repr(val)
80
81
82def PrettyDir(dir_name, home_dir):
83 # type: (str, Optional[str]) -> str
84 """Maybe replace the home dir with ~.
85
86 Used by the 'dirs' builtin and the prompt evaluator.
87 """
88 if home_dir is not None:
89 if dir_name == home_dir or dir_name.startswith(home_dir + '/'):
90 return '~' + dir_name[len(home_dir):]
91
92 return dir_name
93
94
95def PrintCaretLine(line, col, length, f):
96 # type: (str, int, int, mylib.Writer) -> None
97 # preserve tabs
98 for c in line[:col]:
99 f.write('\t' if c == '\t' else ' ')
100 f.write('^')
101 f.write('~' * (length - 1))
102 f.write('\n')
103
104
105def _PrintCodeExcerpt(line, col, length, f):
106 # type: (str, int, int, mylib.Writer) -> None
107
108 buf = mylib.BufWriter()
109
110 # TODO: Be smart about horizontal space when printing code snippet
111 # - Accept max_width param, which is terminal width or perhaps 100
112 # when there's no terminal
113 # - If 'length' of token is greater than max_width, then perhaps print 10
114 # chars on each side
115 # - If len(line) is less than max_width, then print everything normally
116 # - If len(line) is greater than max_width, then print up to max_width
117 # but make sure to include the entire token, with some context
118 # Print > < or ... to show truncation
119 #
120 # ^col 80 ^~~~~ error
121
122 buf.write(' ') # indent
123 buf.write(line.rstrip())
124
125 buf.write('\n ') # indent
126 PrintCaretLine(line, col, length, buf)
127
128 # Do this all in a single write() call so it's less likely to be
129 # interleaved. See test/runtime-errors.sh test-errexit-multiple-processes
130 f.write(buf.getvalue())
131
132
133def _PrintTokenTooLong(loc_tok, f):
134 # type: (loc.TokenTooLong, mylib.Writer) -> None
135 line = loc_tok.line
136 col = loc_tok.col
137
138 buf = mylib.BufWriter()
139
140 buf.write(' ')
141 # Only print 10 characters, since it's probably very long
142 buf.write(line.content[:col + 10].rstrip())
143 buf.write('\n ')
144
145 # preserve tabs, like _PrintCodeExcerpt
146 for c in line.content[:col]:
147 buf.write('\t' if c == '\t' else ' ')
148
149 buf.write('^\n')
150
151 source_str = GetLineSourceString(loc_tok.line, quote_filename=True)
152 buf.write(
153 '%s:%d: Token starting at column %d is too long: %d bytes (%s)\n' %
154 (source_str, line.line_num, loc_tok.col, loc_tok.length,
155 Id_str(loc_tok.id)))
156
157 # single write() call
158 f.write(buf.getvalue())
159
160
161def GetFilenameString(line):
162 # type: (SourceLine) -> str
163 """Get the path of the file that a line appears in.
164
165 Returns "main" if it's stdin or -c
166 Returns "?" if it's not in a file.
167
168 Used by declare -F, with shopt -s extdebug.
169 """
170 src = line.src
171 UP_src = src
172
173 filename_str = '?' # default
174 with tagswitch(src) as case:
175 # Copying bash, it uses the string 'main'.
176 # I think ? would be better here, because this can get confused with a
177 # file 'main'. But it's fine for our task file usage.
178 if case(source_e.CFlag):
179 filename_str = 'main'
180 elif case(source_e.Stdin):
181 filename_str = 'main'
182
183 elif case(source_e.MainFile):
184 src = cast(source.MainFile, UP_src)
185 filename_str = src.path
186 elif case(source_e.OtherFile):
187 src = cast(source.OtherFile, UP_src)
188 filename_str = src.path
189
190 else:
191 pass
192 return filename_str
193
194
195def GetLineSourceString(line, quote_filename=False):
196 # type: (SourceLine, bool) -> str
197 """Returns a human-readable string for dev tools.
198
199 This function is RECURSIVE because there may be dynamic parsing.
200 """
201 src = line.src
202 UP_src = src
203
204 with tagswitch(src) as case:
205 if case(source_e.Interactive):
206 s = '[ interactive ]' # This might need some changes
207 elif case(source_e.Headless):
208 s = '[ headless ]'
209 elif case(source_e.CFlag):
210 s = '[ -c flag ]'
211 elif case(source_e.Stdin):
212 src = cast(source.Stdin, UP_src)
213 s = '[ stdin%s ]' % src.comment
214
215 elif case(source_e.MainFile):
216 src = cast(source.MainFile, UP_src)
217 # This will quote a file called '[ -c flag ]' to disambiguate it!
218 # also handles characters that are unprintable in a terminal.
219 s = src.path
220 if quote_filename:
221 s = j8_lite.EncodeString(s, unquoted_ok=True)
222 elif case(source_e.OtherFile):
223 src = cast(source.OtherFile, UP_src)
224 # ditto
225 s = src.path
226 if quote_filename:
227 s = j8_lite.EncodeString(s, unquoted_ok=True)
228
229 elif case(source_e.Dynamic):
230 src = cast(source.Dynamic, UP_src)
231
232 # Note: _PrintWithLocation() uses this more specifically
233
234 # TODO: check loc.Missing; otherwise get Token from loc_t, then line
235 blame_tok = location.TokenFor(src.location)
236 if blame_tok is None:
237 s = '[ %s at ? ]' % src.what
238 else:
239 line = blame_tok.line
240 line_num = line.line_num
241 outer_source = GetLineSourceString(
242 line, quote_filename=quote_filename)
243 s = '[ %s at line %d of %s ]' % (src.what, line_num,
244 outer_source)
245
246 elif case(source_e.Variable):
247 src = cast(source.Variable, UP_src)
248
249 if src.var_name is None:
250 var_name = '?'
251 else:
252 var_name = repr(src.var_name)
253
254 if src.location.tag() == loc_e.Missing:
255 where = '?'
256 else:
257 blame_tok = location.TokenFor(src.location)
258 assert blame_tok is not None
259 line_num = blame_tok.line.line_num
260 outer_source = GetLineSourceString(
261 blame_tok.line, quote_filename=quote_filename)
262 where = 'line %d of %s' % (line_num, outer_source)
263
264 s = '[ var %s at %s ]' % (var_name, where)
265
266 elif case(source_e.VarRef):
267 src = cast(source.VarRef, UP_src)
268
269 orig_tok = src.orig_tok
270 line_num = orig_tok.line.line_num
271 outer_source = GetLineSourceString(orig_tok.line,
272 quote_filename=quote_filename)
273 where = 'line %d of %s' % (line_num, outer_source)
274
275 var_name = lexer.TokenVal(orig_tok)
276 s = '[ contents of var %r at %s ]' % (var_name, where)
277
278 elif case(source_e.Alias):
279 src = cast(source.Alias, UP_src)
280 s = '[ expansion of alias %r ]' % src.argv0
281
282 elif case(source_e.Reparsed):
283 src = cast(source.Reparsed, UP_src)
284 span2 = src.left_token
285 outer_source = GetLineSourceString(span2.line,
286 quote_filename=quote_filename)
287 s = '[ %s in %s ]' % (src.what, outer_source)
288
289 elif case(source_e.Synthetic):
290 src = cast(source.Synthetic, UP_src)
291 s = '-- %s' % src.s # use -- to say it came from a flag
292
293 else:
294 raise AssertionError(src)
295
296 return s
297
298
299def _PrintWithLocation(prefix, msg, blame_loc, show_code):
300 # type: (str, str, loc_t, bool) -> None
301 """Print an error message attached to a location.
302
303 We may quote code this:
304
305 echo $foo
306 ^~~~
307 [ -c flag ]:1: Failed
308
309 Should we have multiple locations?
310
311 - single line and verbose?
312 - and turn on "stack" tracing? For 'source' and more?
313 """
314 f = mylib.Stderr()
315 if blame_loc.tag() == loc_e.TokenTooLong:
316 # test/spec.sh parse-errors shows this
317 _PrintTokenTooLong(cast(loc.TokenTooLong, blame_loc), f)
318 return
319
320 blame_tok = location.TokenFor(blame_loc)
321 # lexer.DummyToken() gives you a Lit_Chars Token with no line
322 if blame_tok is None or blame_tok.line is None:
323 f.write('[??? no location ???] %s%s\n' % (prefix, msg))
324 return
325
326 orig_col = blame_tok.col
327 src = blame_tok.line.src
328 line = blame_tok.line.content
329 line_num = blame_tok.line.line_num # overwritten by source.Reparsed case
330
331 if show_code:
332 UP_src = src
333
334 with tagswitch(src) as case:
335 if case(source_e.Reparsed):
336 # Special case for LValue/backticks
337
338 # We want the excerpt to look like this:
339 # a[x+]=1
340 # ^
341 # Rather than quoting the internal buffer:
342 # x+
343 # ^
344
345 # Show errors:
346 # test/parse-errors.sh text-arith-context
347
348 src = cast(source.Reparsed, UP_src)
349 tok2 = src.left_token
350 line_num = tok2.line.line_num
351
352 line2 = tok2.line.content
353 lbracket_col = tok2.col + tok2.length
354 # NOTE: The inner line number is always 1 because of reparsing.
355 # We overwrite it with the original token.
356 _PrintCodeExcerpt(line2, orig_col + lbracket_col, 1, f)
357
358 elif case(source_e.Dynamic):
359 src = cast(source.Dynamic, UP_src)
360 # Special case for eval, unset, printf -v, etc.
361
362 # Show errors:
363 # test/runtime-errors.sh test-assoc-array
364
365 #print('OUTER blame_loc', blame_loc)
366 #print('OUTER tok', blame_tok)
367 #print('INNER src.location', src.location)
368
369 # Print code and location for MOST SPECIFIC location
370 _PrintCodeExcerpt(line, blame_tok.col, blame_tok.length, f)
371 source_str = GetLineSourceString(blame_tok.line,
372 quote_filename=True)
373 f.write('%s:%d\n' % (source_str, line_num))
374 f.write('\n')
375
376 # Recursive call: Print OUTER location, with error message
377 _PrintWithLocation(prefix, msg, src.location, show_code)
378 return
379
380 else:
381 _PrintCodeExcerpt(line, blame_tok.col, blame_tok.length, f)
382
383 source_str = GetLineSourceString(blame_tok.line, quote_filename=True)
384
385 # TODO: If the line is blank, it would be nice to print the last non-blank
386 # line too?
387 f.write('%s:%d: %s%s\n' % (source_str, line_num, prefix, msg))
388
389
390def CodeExcerptAndPrefix(blame_tok):
391 # type: (Token) -> Tuple[str, str]
392 """Return a string that quotes code, and a string location prefix.
393
394 Similar logic as _PrintWithLocation, except we know we have a token.
395 """
396 line = blame_tok.line
397
398 buf = mylib.BufWriter()
399 _PrintCodeExcerpt(line.content, blame_tok.col, blame_tok.length, buf)
400
401 source_str = GetLineSourceString(line, quote_filename=True)
402 prefix = '%s:%d: ' % (source_str, blame_tok.line.line_num)
403
404 return buf.getvalue(), prefix
405
406
407class ctx_Location(object):
408
409 def __init__(self, errfmt, location):
410 # type: (ErrorFormatter, loc_t) -> None
411 errfmt.loc_stack.append(location)
412 self.errfmt = errfmt
413
414 def __enter__(self):
415 # type: () -> None
416 pass
417
418 def __exit__(self, type, value, traceback):
419 # type: (Any, Any, Any) -> None
420 self.errfmt.loc_stack.pop()
421
422
423# TODO:
424# - ColorErrorFormatter
425# - BareErrorFormatter? Could just display the foo.sh:37:8: and not quotation.
426#
427# Are these controlled by a flag? It's sort of like --comp-ui. Maybe
428# --error-ui.
429
430
431class ErrorFormatter(object):
432 """Print errors with code excerpts.
433
434 Philosophy:
435 - There should be zero or one code quotation when a shell exits non-zero.
436 Showing the same line twice is noisy.
437 - When running parallel processes, avoid interleaving multi-line code
438 quotations. (TODO: turn off in child processes?)
439 """
440
441 def __init__(self):
442 # type: () -> None
443 self.loc_stack = [] # type: List[loc_t]
444 self.one_line_errexit = False # root process
445
446 def OneLineErrExit(self):
447 # type: () -> None
448 """Unused now.
449
450 For SubprogramThunk.
451 """
452 self.one_line_errexit = True
453
454 # A stack used for the current builtin. A fallback for UsageError.
455 # TODO: Should we have PushBuiltinName? Then we can have a consistent style
456 # like foo.sh:1: (compopt) Not currently executing.
457 def _FallbackLocation(self, blame_loc):
458 # type: (Optional[loc_t]) -> loc_t
459 if blame_loc is None or blame_loc.tag() == loc_e.Missing:
460 if len(self.loc_stack):
461 return self.loc_stack[-1]
462 return loc.Missing
463
464 return blame_loc
465
466 def PrefixPrint(self, msg, prefix, blame_loc):
467 # type: (str, str, loc_t) -> None
468 """Print a hard-coded message with a prefix, and quote code."""
469 _PrintWithLocation(prefix,
470 msg,
471 self._FallbackLocation(blame_loc),
472 show_code=True)
473
474 def Print_(self, msg, blame_loc=None):
475 # type: (str, loc_t) -> None
476 """Print message and quote code."""
477 _PrintWithLocation('',
478 msg,
479 self._FallbackLocation(blame_loc),
480 show_code=True)
481
482 def PrintMessage(self, msg, blame_loc=None):
483 # type: (str, loc_t) -> None
484 """Print a message WITHOUT quoting code."""
485 _PrintWithLocation('',
486 msg,
487 self._FallbackLocation(blame_loc),
488 show_code=False)
489
490 def StderrLine(self, msg):
491 # type: (str) -> None
492 """Just print to stderr."""
493 print_stderr(msg)
494
495 def PrettyPrintError(self, err, prefix=''):
496 # type: (_ErrorWithLocation, str) -> None
497 """Print an exception that was caught, with a code quotation.
498
499 Unlike other methods, this doesn't use the GetLocationForLine()
500 fallback. That only applies to builtins; instead we check
501 e.HasLocation() at a higher level, in CommandEvaluator.
502 """
503 # TODO: Should there be a special span_id of 0 for EOF? runtime.NO_SPID
504 # means there is no location info, but 0 could mean that the location is EOF.
505 # So then you query the arena for the last line in that case?
506 # Eof_Real is the ONLY token with 0 span, because it's invisible!
507 # Well Eol_Tok is a sentinel with span_id == runtime.NO_SPID. I think that
508 # is OK.
509 # Problem: the column for Eof could be useful.
510
511 _PrintWithLocation(prefix, err.UserErrorString(), err.location, True)
512
513 def PrintErrExit(self, err, pid):
514 # type: (error.ErrExit, int) -> None
515
516 # TODO:
517 # - Don't quote code if you already quoted something on the same line?
518 # - _PrintWithLocation calculates the line_id. So you need to remember that?
519 # - return it here?
520 prefix = 'errexit PID %d: ' % pid
521 _PrintWithLocation(prefix, err.UserErrorString(), err.location,
522 err.show_code)
523
524
525def PrintAst(node, flag):
526 # type: (command_t, arg_types.main) -> None
527
528 if flag.ast_format == 'none':
529 print_stderr('AST not printed.')
530 if 0:
531 from _devbuild.gen.id_kind_asdl import Id_str
532 from frontend.lexer import ID_HIST, LAZY_ID_HIST
533
534 print(LAZY_ID_HIST)
535 print(len(LAZY_ID_HIST))
536
537 for id_, count in ID_HIST.most_common(10):
538 print('%8d %s' % (count, Id_str(id_)))
539 print()
540 total = sum(ID_HIST.values())
541 uniq = len(ID_HIST)
542 print('%8d total tokens' % total)
543 print('%8d unique tokens IDs' % uniq)
544 print()
545
546 for id_, count in LAZY_ID_HIST.most_common(10):
547 print('%8d %s' % (count, Id_str(id_)))
548 print()
549 total = sum(LAZY_ID_HIST.values())
550 uniq = len(LAZY_ID_HIST)
551 print('%8d total tokens' % total)
552 print('%8d tokens with LazyVal()' % total)
553 print('%8d unique tokens IDs' % uniq)
554 print()
555
556 if 0:
557 from osh.word_parse import WORD_HIST
558 #print(WORD_HIST)
559 for desc, count in WORD_HIST.most_common(20):
560 print('%8d %s' % (count, desc))
561
562 else: # text output
563 f = mylib.Stdout()
564
565 do_abbrev = 'abbrev-' in flag.ast_format
566 perf_stats = flag.ast_format.startswith('__') # __perf or __dumpdoc
567
568 if perf_stats:
569 log('')
570 log('___ GC: after parsing')
571 mylib.PrintGcStats()
572 log('')
573
574 tree = node.PrettyTree(do_abbrev)
575
576 if perf_stats:
577 # Warning: __dumpdoc should only be passed with tiny -c fragments.
578 # This tree is huge and can eat up all memory.
579 fmt._HNodePrettyPrint(True,
580 flag.ast_format == '__dumpdoc',
581 tree,
582 f,
583 max_width=_GetMaxWidth())
584 else:
585 fmt.HNodePrettyPrint(tree, f, max_width=_GetMaxWidth())
586
587
588def TypeNotPrinted(val):
589 # type: (value_t) -> bool
590 return val.tag() in (value_e.Null, value_e.Bool, value_e.Int,
591 value_e.Float, value_e.Str, value_e.List,
592 value_e.Dict, value_e.Obj)
593
594
595def _GetMaxWidth():
596 # type: () -> int
597 max_width = 80 # default value
598 try:
599 width = libc.get_terminal_width()
600 if width > 0:
601 max_width = width
602 except (IOError, OSError):
603 pass # leave at default
604
605 return max_width
606
607
608def PrettyPrintValue(prefix, val, f, max_width=-1):
609 # type: (str, value_t, mylib.Writer, int) -> None
610 """For the = keyword"""
611
612 encoder = pp_value.ValueEncoder()
613 encoder.SetUseStyles(f.isatty())
614
615 # TODO: pretty._Concat, etc. shouldn't be private
616 if TypeNotPrinted(val):
617 mdocs = encoder.TypePrefix(pp_value.ValType(val))
618 mdocs.append(encoder.Value(val))
619 doc = pretty._Concat(mdocs)
620 else:
621 doc = encoder.Value(val)
622
623 if len(prefix):
624 # If you want the type name to be indented, which we don't
625 # inner = pretty._Concat([pretty._Break(""), doc])
626
627 doc = pretty._Concat([
628 pretty.AsciiText(prefix),
629 #pretty._Break(""),
630 pretty._Indent(4, doc)
631 ])
632
633 if max_width == -1:
634 max_width = _GetMaxWidth()
635
636 printer = pretty.PrettyPrinter(max_width)
637
638 buf = mylib.BufWriter()
639 printer.PrintDoc(doc, buf)
640 f.write(buf.getvalue())
641 f.write('\n')
642
643
644def PrintShFunction(proc_val):
645 # type: (value.Proc) -> None
646 if proc_val.code_str is not None:
647 print(proc_val.code_str)
648 else:
649 print('%s() { : "function body not available"; }' % proc_val.name)