OILS / core / ui.py View on Github | oilshell.org

462 lines, 247 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, value_str)
25from asdl import format as fmt
26from data_lang import pretty
27from frontend import lexer
28from frontend import location
29from mycpp import mylib
30from mycpp.mylib import print_stderr, tagswitch, log
31from data_lang import j8
32from data_lang import j8_lite
33
34from typing import List, Optional, Any, cast, TYPE_CHECKING
35if TYPE_CHECKING:
36 from _devbuild.gen import arg_types
37 from core import error
38 from core.error import _ErrorWithLocation
39 from mycpp.mylib import Writer
40
41_ = log
42
43
44def ValType(val):
45 # type: (value_t) -> str
46 """For displaying type errors in the UI."""
47
48 return value_str(val.tag(), dot=False)
49
50
51def CommandType(cmd):
52 # type: (command_t) -> str
53 """For displaying commands in the UI."""
54
55 # Displays 'command.Simple' for now, maybe change it.
56 return command_str(cmd.tag())
57
58
59def PrettyId(id_):
60 # type: (Id_t) -> str
61 """For displaying type errors in the UI."""
62
63 # Displays 'Id.BoolUnary_v' for now
64 return Id_str(id_)
65
66
67def PrettyToken(tok):
68 # type: (Token) -> str
69 """Returns a readable token value for the user.
70
71 For syntax errors.
72 """
73 if tok.id == Id.Eof_Real:
74 return 'EOF'
75
76 val = tok.line.content[tok.col:tok.col + tok.length]
77 # TODO: Print length 0 as 'EOF'?
78 return repr(val)
79
80
81def PrettyDir(dir_name, home_dir):
82 # type: (str, Optional[str]) -> str
83 """Maybe replace the home dir with ~.
84
85 Used by the 'dirs' builtin and the prompt evaluator.
86 """
87 if home_dir is not None:
88 if dir_name == home_dir or dir_name.startswith(home_dir + '/'):
89 return '~' + dir_name[len(home_dir):]
90
91 return dir_name
92
93
94def _PrintCodeExcerpt(line, col, length, f):
95 # type: (str, int, int, Writer) -> None
96
97 buf = mylib.BufWriter()
98
99 buf.write(' ')
100 buf.write(line.rstrip())
101 buf.write('\n ')
102 # preserve tabs
103 for c in line[:col]:
104 buf.write('\t' if c == '\t' else ' ')
105 buf.write('^')
106 buf.write('~' * (length - 1))
107 buf.write('\n')
108
109 # Do this all in a single write() call so it's less likely to be
110 # interleaved. See test/runtime-errors.sh errexit_multiple_processes
111 f.write(buf.getvalue())
112
113
114def GetLineSourceString(line, quote_filename=False):
115 # type: (SourceLine, bool) -> str
116 """Returns a human-readable string for dev tools.
117
118 This function is RECURSIVE because there may be dynamic parsing.
119 """
120 src = line.src
121 UP_src = src
122
123 with tagswitch(src) as case:
124 if case(source_e.Interactive):
125 s = '[ interactive ]' # This might need some changes
126 elif case(source_e.Headless):
127 s = '[ headless ]'
128 elif case(source_e.CFlag):
129 s = '[ -c flag ]'
130 elif case(source_e.Stdin):
131 src = cast(source.Stdin, UP_src)
132 s = '[ stdin%s ]' % src.comment
133
134 elif case(source_e.MainFile):
135 src = cast(source.MainFile, UP_src)
136 # This will quote a file called '[ -c flag ]' to disambiguate it!
137 # also handles characters that are unprintable in a terminal.
138 s = src.path
139 if quote_filename:
140 s = j8_lite.EncodeString(s, unquoted_ok=True)
141 elif case(source_e.SourcedFile):
142 src = cast(source.SourcedFile, UP_src)
143 # ditto
144 s = src.path
145 if quote_filename:
146 s = j8_lite.EncodeString(s, unquoted_ok=True)
147
148 elif case(source_e.ArgvWord):
149 src = cast(source.ArgvWord, UP_src)
150
151 # TODO: check loc.Missing; otherwise get Token from loc_t, then line
152 blame_tok = location.TokenFor(src.location)
153 if blame_tok is None:
154 s = '[ %s word at ? ]' % src.what
155 else:
156 line = blame_tok.line
157 line_num = line.line_num
158 outer_source = GetLineSourceString(
159 line, quote_filename=quote_filename)
160 s = '[ %s word at line %d of %s ]' % (src.what, line_num,
161 outer_source)
162 # Note: _PrintCodeExcerpt called above
163
164 elif case(source_e.Variable):
165 src = cast(source.Variable, UP_src)
166
167 if src.var_name is None:
168 var_name = '?'
169 else:
170 var_name = repr(src.var_name)
171
172 if src.location.tag() == loc_e.Missing:
173 where = '?'
174 else:
175 blame_tok = location.TokenFor(src.location)
176 assert blame_tok is not None
177 line_num = blame_tok.line.line_num
178 outer_source = GetLineSourceString(
179 blame_tok.line, quote_filename=quote_filename)
180 where = 'line %d of %s' % (line_num, outer_source)
181
182 s = '[ var %s at %s ]' % (var_name, where)
183
184 elif case(source_e.VarRef):
185 src = cast(source.VarRef, UP_src)
186
187 orig_tok = src.orig_tok
188 line_num = orig_tok.line.line_num
189 outer_source = GetLineSourceString(orig_tok.line,
190 quote_filename=quote_filename)
191 where = 'line %d of %s' % (line_num, outer_source)
192
193 var_name = lexer.TokenVal(orig_tok)
194 s = '[ contents of var %r at %s ]' % (var_name, where)
195
196 elif case(source_e.Alias):
197 src = cast(source.Alias, UP_src)
198 s = '[ expansion of alias %r ]' % src.argv0
199
200 elif case(source_e.Reparsed):
201 src = cast(source.Reparsed, UP_src)
202 span2 = src.left_token
203 outer_source = GetLineSourceString(span2.line,
204 quote_filename=quote_filename)
205 s = '[ %s in %s ]' % (src.what, outer_source)
206
207 elif case(source_e.Synthetic):
208 src = cast(source.Synthetic, UP_src)
209 s = '-- %s' % src.s # use -- to say it came from a flag
210
211 else:
212 raise AssertionError(src)
213
214 return s
215
216
217def _PrintWithLocation(prefix, msg, blame_loc, show_code):
218 # type: (str, str, loc_t, bool) -> None
219 """Should we have multiple error formats:
220
221 - single line and verbose?
222 - and turn on "stack" tracing? For 'source' and more?
223 """
224 f = mylib.Stderr()
225 blame_tok = location.TokenFor(blame_loc)
226 if blame_tok is None: # When does this happen?
227 f.write('[??? no location ???] %s%s\n' % (prefix, msg))
228 return
229
230 orig_col = blame_tok.col
231 src = blame_tok.line.src
232 line = blame_tok.line.content
233 line_num = blame_tok.line.line_num # overwritten by source__LValue case
234
235 if show_code:
236 UP_src = src
237 # LValue/backticks is the only case where we don't print this
238 if src.tag() == source_e.Reparsed:
239 src = cast(source.Reparsed, UP_src)
240 span2 = src.left_token
241 line_num = span2.line.line_num
242
243 # We want the excerpt to look like this:
244 # a[x+]=1
245 # ^
246 # Rather than quoting the internal buffer:
247 # x+
248 # ^
249 line2 = span2.line.content
250 lbracket_col = span2.col + span2.length
251 # NOTE: The inner line number is always 1 because of reparsing. We
252 # overwrite it with the original span.
253 _PrintCodeExcerpt(line2, orig_col + lbracket_col, 1, f)
254
255 else:
256 _PrintCodeExcerpt(line, blame_tok.col, blame_tok.length, f)
257
258 source_str = GetLineSourceString(blame_tok.line, quote_filename=True)
259
260 # TODO: If the line is blank, it would be nice to print the last non-blank
261 # line too?
262 f.write('%s:%d: %s%s\n' % (source_str, line_num, prefix, msg))
263
264
265class ctx_Location(object):
266
267 def __init__(self, errfmt, location):
268 # type: (ErrorFormatter, loc_t) -> None
269 errfmt.loc_stack.append(location)
270 self.errfmt = errfmt
271
272 def __enter__(self):
273 # type: () -> None
274 pass
275
276 def __exit__(self, type, value, traceback):
277 # type: (Any, Any, Any) -> None
278 self.errfmt.loc_stack.pop()
279
280
281# TODO:
282# - ColorErrorFormatter
283# - BareErrorFormatter? Could just display the foo.sh:37:8: and not quotation.
284#
285# Are these controlled by a flag? It's sort of like --comp-ui. Maybe
286# --error-ui.
287
288
289class ErrorFormatter(object):
290 """Print errors with code excerpts.
291
292 Philosophy:
293 - There should be zero or one code quotation when a shell exits non-zero.
294 Showing the same line twice is noisy.
295 - When running parallel processes, avoid interleaving multi-line code
296 quotations. (TODO: turn off in child processes?)
297 """
298
299 def __init__(self):
300 # type: () -> None
301 self.loc_stack = [] # type: List[loc_t]
302 self.one_line_errexit = False # root process
303
304 def OneLineErrExit(self):
305 # type: () -> None
306 """Unused now.
307
308 For SubprogramThunk.
309 """
310 self.one_line_errexit = True
311
312 # A stack used for the current builtin. A fallback for UsageError.
313 # TODO: Should we have PushBuiltinName? Then we can have a consistent style
314 # like foo.sh:1: (compopt) Not currently executing.
315 def _FallbackLocation(self, blame_loc):
316 # type: (Optional[loc_t]) -> loc_t
317 if blame_loc is None or blame_loc.tag() == loc_e.Missing:
318 if len(self.loc_stack):
319 return self.loc_stack[-1]
320 return loc.Missing
321
322 return blame_loc
323
324 def PrefixPrint(self, msg, prefix, blame_loc):
325 # type: (str, str, loc_t) -> None
326 """Print a hard-coded message with a prefix, and quote code."""
327 _PrintWithLocation(prefix,
328 msg,
329 self._FallbackLocation(blame_loc),
330 show_code=True)
331
332 def Print_(self, msg, blame_loc=None):
333 # type: (str, loc_t) -> None
334 """Print message and quote code."""
335 _PrintWithLocation('',
336 msg,
337 self._FallbackLocation(blame_loc),
338 show_code=True)
339
340 def PrintMessage(self, msg, blame_loc=None):
341 # type: (str, loc_t) -> None
342 """Print a message WITHOUT quoting code."""
343 _PrintWithLocation('',
344 msg,
345 self._FallbackLocation(blame_loc),
346 show_code=False)
347
348 def StderrLine(self, msg):
349 # type: (str) -> None
350 """Just print to stderr."""
351 print_stderr(msg)
352
353 def PrettyPrintError(self, err, prefix=''):
354 # type: (_ErrorWithLocation, str) -> None
355 """Print an exception that was caught, with a code quotation.
356
357 Unlike other methods, this doesn't use the GetLocationForLine()
358 fallback. That only applies to builtins; instead we check
359 e.HasLocation() at a higher level, in CommandEvaluator.
360 """
361 # TODO: Should there be a special span_id of 0 for EOF? runtime.NO_SPID
362 # means there is no location info, but 0 could mean that the location is EOF.
363 # So then you query the arena for the last line in that case?
364 # Eof_Real is the ONLY token with 0 span, because it's invisible!
365 # Well Eol_Tok is a sentinel with span_id == runtime.NO_SPID. I think that
366 # is OK.
367 # Problem: the column for Eof could be useful.
368
369 _PrintWithLocation(prefix, err.UserErrorString(), err.location, True)
370
371 def PrintErrExit(self, err, pid):
372 # type: (error.ErrExit, int) -> None
373
374 # TODO:
375 # - Don't quote code if you already quoted something on the same line?
376 # - _PrintWithLocation calculates the line_id. So you need to remember that?
377 # - return it here?
378 prefix = 'errexit PID %d: ' % pid
379 _PrintWithLocation(prefix, err.UserErrorString(), err.location,
380 err.show_code)
381
382
383def PrintAst(node, flag):
384 # type: (command_t, arg_types.main) -> None
385
386 if flag.ast_format == 'none':
387 print_stderr('AST not printed.')
388 if 0:
389 from _devbuild.gen.id_kind_asdl import Id_str
390 from frontend.lexer import ID_HIST
391 for id_, count in ID_HIST.most_common(10):
392 print('%8d %s' % (count, Id_str(id_)))
393 print()
394 total = sum(ID_HIST.values())
395 print('%8d total tokens returned' % total)
396
397 else: # text output
398 f = mylib.Stdout()
399
400 afmt = flag.ast_format # note: mycpp rewrite to avoid 'in'
401 if afmt in ('text', 'abbrev-text'):
402 ast_f = fmt.DetectConsoleOutput(f)
403 elif afmt in ('html', 'abbrev-html'):
404 ast_f = fmt.HtmlOutput(f)
405 else:
406 raise AssertionError()
407
408 if 'abbrev-' in afmt:
409 # ASDL "abbreviations" are only supported by asdl/gen_python.py
410 if mylib.PYTHON:
411 tree = node.AbbreviatedTree()
412 else:
413 tree = node.PrettyTree()
414 else:
415 tree = node.PrettyTree()
416
417 ast_f.FileHeader()
418 fmt.PrintTree(tree, ast_f)
419 ast_f.FileFooter()
420 ast_f.write('\n')
421
422
423def PrettyPrintValue(val, f):
424 # type: (value_t, mylib.Writer) -> None
425 """For the = keyword"""
426
427 ysh_type = ValType(val)
428 id_str = j8.ValueIdString(val)
429
430 UP_val = val
431 with tagswitch(val) as case:
432 # "JSON" data types will use J8 serialization
433 if case(value_e.Null, value_e.Bool, value_e.Int, value_e.Float,
434 value_e.Str, value_e.List, value_e.Dict):
435 # Use () instead of <> as a hint that it's a "JSON value"
436 f.write('(%s%s) ' % (ysh_type, id_str))
437
438 # Unused STUB
439 doc = pretty.FromValue(val)
440
441 buf = mylib.BufWriter()
442
443 # TODO: Wrap lines, and show color. Use core/ansi.py
444 p = j8.InstancePrinter(buf, -1, j8.SHOW_CYCLES | j8.SHOW_NON_DATA)
445
446 # error.Encode should be impossible - we show cycles and non-data
447 p.Print(val)
448
449 f.write(buf.getvalue())
450 f.write('\n')
451
452 elif case(value_e.Range):
453 val = cast(value.Range, UP_val)
454
455 # Printing Range values more nicely. Note that pp line (x) doesn't
456 # have this.
457 f.write('(%s) %d .. %d\n' % (ysh_type, val.lower, val.upper))
458
459 else:
460 # Just print object and ID. Use <> to show that it's more like
461 # a reference type.
462 f.write('<%s%s>\n' % (ysh_type, id_str))