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

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