from ...typecheck import* from ...import ( core, dap, ui, ) from ..views import css from ..variables import VariableComponent, Variable from ..autocomplete import Autocomplete import re import webbrowser import sublime Source = Tuple[dap.Source, Optional[int]] url_matching_regex = re.compile(r"((http|ftp|https)://([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:/~+#-]*[\w@?^=%&/~+#-])?)") # from https://stackoverflow.com/questions/6038061/regular-expression-to-find-urls-within-a-string default_line_regex = re.compile("(.*):([0-9]+):([0-9]+): error: (.*)") class Line: def __init__(self, type: Optional[str]): self.type = type self.line = '' self.source = None #type: Optional[Source] self.variable = None #type: Optional[Variable] self.finished = False def add(self, text: str, source: Optional[Source], line_regex): if self.finished: raise core.Error('line is already complete') self.source = self.source or source self.line += text.rstrip('\r\n') if text[-1] == '\n' or text[-1] == '\r': self.finished = True if not self.source and line_regex: match = line_regex.match(self.line) if match: source = (dap.Source(None, match.group(1), 0, 0, None, []), int(match.group(2))) line = int(match.group(2)) self.line = match.group(4) self.source = source def add_variable(self, variable: Variable, source: Optional[Source]): if self.finished: raise core.Error('line is already complete') self.finished = True self.variable = variable self.source = source class Terminal: def __init__(self, name: str): self.lines = [] #type: List[Line] self._name = name self.on_updated = core.Event() #type: core.Event[None] self.line_regex = default_line_regex self.new_line = True self.escape_input = True def name(self) -> str: return self._name def clicked_source(self, source: dap.Source, line: Optional[int]) -> None: pass def _add_line(self, type: str, text: str, source: Optional[Source] = None): if self.lines: previous = self.lines[-1] if not previous.finished and previous.type == type: previous.add(text, source, self.line_regex) return line = Line(type) line.add(text, source, self.line_regex) self.lines.append(line) self.on_updated.post() def add(self, type: str, text: str, source: Optional[Source] = None): lines = text.splitlines(keepends=True) for line in lines: self._add_line(type, line, source) source = None def add_variable(self, variable, source: Optional[Source] = None): line = Line(None) line.add_variable(variable, source) self.lines.append(line) self.on_updated.post() def clear(self) -> None: self.lines = [] self.on_updated() def writeable(self) -> bool: return False def can_escape_input(self) -> bool: return False def writeable_prompt(self) -> str: return "" def write(self, text: str): assert False, "Panel doesn't support writing" def dispose(self): pass _css_for_type = { "console": css.label, "stderr": css.label_redish, "stdout": css.label, "debugger.error": css.label_redish_secondary, "debugger.info": css.label_secondary, "debugger.output": css.label_secondary, } class LineSourceView (ui.span): def __init__(self, name: str, line: Optional[int], text_width: int, on_clicked_source): super().__init__() self.on_clicked_source = on_clicked_source self.name = name self.line = line self.text_width = text_width def render(self) -> ui.span.Children: if self.line: source_text = "{}@{}".format(self.name, self.line) else: source_text = self.name return [ ui.click(self.on_clicked_source)[ ui.text(source_text, css=css.label_secondary_padding) ] ] class LineView (ui.div): def __init__(self, line: Line, max_line_length: int, on_clicked_source: Callable[[dap.Source, Optional[int]], None]) -> None: super().__init__() self.line = line self.css = _css_for_type.get(line.type, css.label_secondary) self.max_line_length = max_line_length self.on_clicked_source = on_clicked_source self.clicked_menu = None def get(self) -> ui.div.Children: if self.line.variable: source = self.line.source source_item = None if source: def on_clicked_source(): self.on_clicked_source(source[0], source[1]) source_item = LineSourceView(source[0].name or '??', source[1], 15, on_clicked_source) component = VariableComponent(self.line.variable, item_right=source_item) return [component] span_lines = [] #type: List[ui.div] spans = [] #type: List[ui.span] max_line_length = self.max_line_length leftover_line_length = max_line_length # if we have a name/line put it to the right of the first line source = self.line.source if source: leftover_line_length -= 15 def add_name_and_line_if_needed(padding): if not span_lines and source: def on_clicked_source(): self.on_clicked_source(source[0], source[1]) spans.append(LineSourceView(source[0].name or '??', source[1], 15, on_clicked_source)) span_offset = 0 line_text = self.line.line while span_offset < len(line_text): if leftover_line_length <= 0: add_name_and_line_if_needed(0) span_lines.append(ui.div(height=css.row_height)[spans]) spans = [] leftover_line_length = max_line_length text = line_text[span_offset:span_offset + leftover_line_length] span_offset += len(text) spans.append(ui.click(lambda text=text: self.click(text))[ ui.text(text, css=self.css) ]) leftover_line_length -= len(text) add_name_and_line_if_needed(leftover_line_length) span_lines.append(ui.div(height=css.row_height)[spans]) if len(span_lines) == 1: return span_lines span_lines.reverse() return span_lines @core.schedule async def click(self, text: str): values = [ ui.InputListItem(lambda: sublime.set_clipboard(text), "Copy"), ] for match in url_matching_regex.findall(text): values.insert(0, ui.InputListItem(lambda: webbrowser.open_new_tab(match[0]), "Open")) if self.clicked_menu: values[0].run() self.clicked_menu.cancel() return values[0].text += "\t Click again to select" self.clicked_menu = ui.InputList(values, text).run() await self.clicked_menu self.clicked_menu = None class TerminalView (ui.div): def __init__(self, terminal: Terminal, on_clicked_source: Callable[[dap.Source, Optional[int]], None]) -> None: super().__init__() self.terminal = terminal self.terminal.on_updated.add(self._on_updated_terminal) self.start_line = 0 self.on_clicked_source = on_clicked_source def _on_updated_terminal(self): self.dirty() def on_input(self): label = self.terminal.writeable_prompt() def run(value: str): if not value: return self.terminal.write(value) self.on_input() ui.InputText(run, label, enable_when_active=Autocomplete.for_window(sublime.active_window())).run() def on_toggle_input_mode(self): self.terminal.escape_input = not self.terminal.escape_input self.dirty() def action_buttons(self) -> List[Tuple[ui.Image, Callable]]: return [ (ui.Images.shared.up, self.on_up), (ui.Images.shared.down, self.on_down), (ui.Images.shared.clear, self.on_clear), ] def on_up(self) -> None: self.start_line += 10 self.dirty() def on_down(self) -> None: self.start_line -= 10 self.dirty() def on_clear(self) -> None: self.terminal.clear() def render(self): assert self.layout lines = [] height = 0 max_height = int((self.layout.height() - css.header_height)/css.row_height) - 1.0 count = len(self.terminal.lines) start = 0 from ..views.layout import console_panel_width width = self.width(self.layout) max_line_length = int(width) if count > max_height: start = self.start_line for line in self.terminal.lines[::-1][start:]: for l in LineView(line, max_line_length, self.on_clicked_source).get(): height += 1 lines.append(l) if height >= max_height: break if height >= max_height: break lines.reverse() if self.terminal.writeable(): input_line = [] if self.terminal.can_escape_input(): if self.terminal.escape_input: text = 'esc' else: text = 'line' mode_toggle = ui.span(css=css.button)[ui.click(self.on_toggle_input_mode)[ ui.text(text, css=css.label_secondary), ]] input_line.append(mode_toggle) label = self.terminal.writeable_prompt() input_line.append( ui.click(self.on_input)[ ui.icon(ui.Images.shared.right), ui.text(label, css=css.label_secondary_padding), ] ) lines.append(ui.div(height=css.row_height)[input_line]) return lines