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

309 lines, 151 significant
1#!/usr/bin/env python2
2from __future__ import print_function
3"""pp_hnode.py - Base class for pretty printing, and HNodeEncoder"""
4
5from _devbuild.gen.hnode_asdl import hnode, hnode_e, hnode_t, Field, color_e
6from _devbuild.gen.pretty_asdl import (doc, MeasuredDoc, Measure)
7from data_lang import j8_lite
8from display import ansi
9from display import pretty
10from display.pretty import (_Break, _Concat, _Flat, _Group, _IfFlat, _Indent,
11 _EmptyMeasure, AsciiText)
12from mycpp import mylib
13from mycpp.mylib import log, tagswitch, switch
14from typing import cast, List, Dict, Optional
15
16_ = log
17
18
19class BaseEncoder(object):
20
21 def __init__(self):
22 # type: () -> None
23
24 # Default values
25 self.indent = 4
26 self.use_styles = True
27 # Tuned for 'data_lang/pretty-benchmark.sh float-demo'
28 # TODO: might want options for float width
29 self.max_tabular_width = 22
30
31 self.visiting = {} # type: Dict[int, bool]
32
33 def SetIndent(self, indent):
34 # type: (int) -> None
35 """Set the number of spaces per indent."""
36 self.indent = indent
37
38 def SetUseStyles(self, use_styles):
39 # type: (bool) -> None
40 """Print with ansi colors and styles, rather than plain text."""
41 self.use_styles = use_styles
42
43 def SetMaxTabularWidth(self, max_tabular_width):
44 # type: (int) -> None
45 """Set the maximum width that list elements can be, for them to be
46 vertically aligned."""
47 self.max_tabular_width = max_tabular_width
48
49 def _Styled(self, style, mdoc):
50 # type: (str, MeasuredDoc) -> MeasuredDoc
51 """Apply the ANSI style string to the given node, if use_styles is set."""
52 if self.use_styles:
53 return _Concat([
54 MeasuredDoc(doc.Text(style), _EmptyMeasure()), mdoc,
55 MeasuredDoc(doc.Text(ansi.RESET), _EmptyMeasure())
56 ])
57 else:
58 return mdoc
59
60 def _StyledAscii(self, style, s):
61 # type: (str, str) -> MeasuredDoc
62 """Apply the ANSI style string to the given node, if use_styles is set."""
63 measure = Measure(len(s), -1) # like AsciiText()
64 if self.use_styles:
65 s = '%s%s%s' % (style, s, ansi.RESET)
66 return MeasuredDoc(doc.Text(s), measure)
67
68 def _Surrounded(self, left, mdoc, right):
69 # type: (str, MeasuredDoc, str) -> MeasuredDoc
70 """Print one of two options (using '[', ']' for left, right):
71
72 [mdoc]
73 ------
74 [
75 mdoc
76 ]
77 """
78 # TODO:
79 # - left and right AsciiText often CONSTANT mdocs
80 # - _Break too
81 return _Group(
82 _Concat([
83 AsciiText(left),
84 _Indent(self.indent, _Concat([_Break(''), mdoc])),
85 _Break(''),
86 AsciiText(right)
87 ]))
88
89 def _SurroundedAndPrefixed(self, left, prefix, sep, mdoc, right):
90 # type: (str, MeasuredDoc, str, MeasuredDoc, str) -> MeasuredDoc
91 """Print one of two options
92 (using '[', 'prefix', ':', 'mdoc', ']' for left, prefix, sep, mdoc, right):
93
94 [prefix:mdoc]
95 ------
96 [prefix
97 mdoc
98 ]
99 """
100 return _Group(
101 _Concat([
102 AsciiText(left), prefix,
103 _Indent(self.indent, _Concat([_Break(sep), mdoc])),
104 _Break(''),
105 AsciiText(right)
106 ]))
107
108 def _Join(self, items, sep, space):
109 # type: (List[MeasuredDoc], str, str) -> MeasuredDoc
110 """Join `items`, using either 'sep+space' or 'sep+newline' between them.
111
112 e.g., if sep and space are ',' and '_', print one of these two cases:
113
114 first,_second,_third
115 ------
116 first,
117 second,
118 third
119 """
120 seq = [] # type: List[MeasuredDoc]
121 for i, item in enumerate(items):
122 if i != 0:
123 seq.append(AsciiText(sep))
124 seq.append(_Break(space))
125 seq.append(item)
126 return _Concat(seq)
127
128 def _Tabular(self, items, sep):
129 # type: (List[MeasuredDoc], str) -> MeasuredDoc
130 """Join `items` together, using one of three styles:
131
132 (showing spaces as underscores for clarity)
133
134 first,_second,_third,_fourth,_fifth,_sixth,_seventh,_eighth
135 ------
136 first,___second,__third,
137 fourth,__fifth,___sixth,
138 seventh,_eighth
139 ------
140 first,
141 second,
142 third,
143 fourth,
144 fifth,
145 sixth,
146 seventh,
147 eighth
148
149 The first "single line" style is used if the items fit on one line. The
150 second "tabular" style is used if the flat width of all items is no
151 greater than self.max_tabular_width. The third "multi line" style is
152 used otherwise.
153 """
154 # Why not "just" use tabular alignment so long as two items fit on every
155 # line? Because it isn't possible to check for that in the pretty
156 # printing language. There are two sorts of conditionals we can do:
157 #
158 # A. Inside the pretty printing language, which supports exactly one
159 # conditional: "does it fit on one line?".
160 # B. Outside the pretty printing language we can run arbitrary Python
161 # code, but we don't know how much space is available on the line
162 # because it depends on the context in which we're printed, which may
163 # vary.
164 #
165 # We're picking between the three styles, by using (A) to check if the
166 # first style fits on one line, then using (B) with "are all the items
167 # smaller than self.max_tabular_width?" to pick between style 2 and
168 # style 3.
169
170 if len(items) == 0:
171 # TODO: this should be turned into "append nothing"
172 return AsciiText('')
173
174 max_flat_len = 0
175 seq = [] # type: List[MeasuredDoc]
176 for i, item in enumerate(items):
177 if i != 0:
178 seq.append(AsciiText(sep))
179 seq.append(_Break(' '))
180 # It would be nice if we could extend other _Concat() nodes here
181 seq.append(item)
182
183 max_flat_len = max(max_flat_len, item.measure.flat)
184
185 non_tabular = _Concat(seq)
186
187 #log('MAX FLAT %d', max_flat_len)
188
189 sep_width = len(sep)
190 if max_flat_len + sep_width + 1 <= self.max_tabular_width:
191 tabular_seq = [] # type: List[MeasuredDoc]
192 for i, item in enumerate(items):
193 tabular_seq.append(_Flat(item))
194 if i != len(items) - 1:
195 padding = max_flat_len - item.measure.flat + 1
196 tabular_seq.append(AsciiText(sep))
197 tabular_seq.append(_Group(_Break(' ' * padding)))
198 tabular = _Concat(tabular_seq)
199 return _Group(_IfFlat(non_tabular, tabular))
200 else:
201 return non_tabular
202
203
204class HNodeEncoder(BaseEncoder):
205
206 def __init__(self):
207 # type: () -> None
208 BaseEncoder.__init__(self)
209
210 self.type_color = ansi.YELLOW
211 self.field_color = ansi.MAGENTA
212
213 def HNode(self, h):
214 # type: (hnode_t) -> MeasuredDoc
215 self.visiting.clear()
216 return self._HNode(h)
217
218 def _Field(self, field):
219 # type: (Field) -> MeasuredDoc
220 name = AsciiText(field.name + ':')
221
222 # TODO: the _HNode is often a _Concat node, and we could optimize them
223 # together. That means we also have to concatenate their measures.
224 return _Concat([name, self._HNode(field.val)])
225
226 def _HNode(self, h):
227 # type: (hnode_t) -> MeasuredDoc
228
229 UP_h = h
230 with tagswitch(h) as case:
231 if case(hnode_e.AlreadySeen):
232 h = cast(hnode.AlreadySeen, UP_h)
233 return pretty.AsciiText('...0x%s' % mylib.hex_lower(h.heap_id))
234
235 elif case(hnode_e.Leaf):
236 h = cast(hnode.Leaf, UP_h)
237
238 with switch(h.color) as case2:
239 if case2(color_e.TypeName):
240 color = ansi.YELLOW
241 elif case2(color_e.StringConst):
242 color = ansi.BOLD
243 elif case2(color_e.OtherConst):
244 color = ansi.GREEN
245 elif case2(color_e.External):
246 color = ansi.BOLD + ansi.BLUE
247 elif case2(color_e.UserType):
248 color = ansi.GREEN # Same color as other literals for now
249 else:
250 raise AssertionError()
251
252 # TODO: what do we do with node.color
253 s = j8_lite.EncodeString(h.s, unquoted_ok=True)
254
255 # Could be Unicode, but we don't want that dependency right now
256 return self._StyledAscii(color, s)
257 #return self._Styled(color, AsciiText(s))
258
259 elif case(hnode_e.Array):
260 h = cast(hnode.Array, UP_h)
261
262 # Reduces Max RSS! Because we build up the trees all at once,
263 # and there's a ton fo garbage.
264 mylib.MaybeCollect()
265
266 if len(h.children) == 0:
267 return AsciiText('[]')
268 children = [self._HNode(item) for item in h.children]
269 return self._Surrounded('[', self._Tabular(children, ''), ']')
270
271 elif case(hnode_e.Record):
272 h = cast(hnode.Record, UP_h)
273
274 type_name = None # type: Optional[MeasuredDoc]
275 if len(h.node_type):
276 type_name = self._StyledAscii(self.type_color, h.node_type)
277 #type_name = self._Styled(self.type_color, AsciiText(h.node_type))
278
279 mdocs = None # type: Optional[List[MeasuredDoc]]
280 if h.unnamed_fields is not None and len(h.unnamed_fields):
281 mdocs = [self._HNode(item) for item in h.unnamed_fields]
282 elif len(h.fields) != 0:
283 mdocs = [self._Field(field) for field in h.fields]
284
285 if mdocs is None:
286 m = [AsciiText(h.left)]
287 if type_name is not None: # {}
288 m.append(type_name)
289 m.append(AsciiText(h.right))
290
291 # e.g. (value.Stdin) with no fields
292 return _Concat(m)
293
294 # Named or unnamed
295 child = self._Join(mdocs, '', ' ')
296
297 if type_name is not None:
298 # e.g. (Token id:LitChars col:5)
299 return self._SurroundedAndPrefixed(h.left, type_name, ' ',
300 child, h.right)
301 else:
302 # e.g. <Id.Lit_Chars foo>
303 return self._Surrounded(h.left, child, h.right)
304
305 else:
306 raise AssertionError()
307
308
309# vim: sw=4