import os.path import color import highlight import regex from point import Point from render import RenderString CONTEXT = 2 # note about the cursor: the cursor position will insert in front of the # character it highlights. to this end, it needs to be able to highlight behind # the last character on a line. thus, the x coordinate of the (logical) cursor # can equal the length of lines[y], even though lines[y][x] throws an index # error. both buffer and window need to be aware of this possibility. # # furthermore, when moving down from a long line to a shorter one, the cursor # will jump in to the end of the shorter line. however, moving back up will # jump back out to its previous position. in these cases the cursor stores the # original x value, but the logical cursor function maps x to the "last # character on the line" for the shorter line. any line movement or actions # which modify the x coordinate will "set" the cursor to a new position, and the # old x value will be forgotten. class Window(object): margins_visible = False def __init__(self, b, a, height=24, width=80, mode_name=None): self.buffer = b self.application = a self.height = height self.width = width self.first = Point(0, 0) self.last = None self.cursor = Point(0, 0) self.mark = None self.active_point = None self.input_line = "" if mode_name is not None: pass elif hasattr(b, 'modename') and b.modename is not None: mode_name = b.modename elif b.btype == 'mini': mode_name = 'mini' elif b.btype == 'console': mode_name = "fundamental" elif b.btype == 'dir': mode_name = 'dir' elif hasattr(b, 'path'): path = b.path basename = os.path.basename(path) ext = self._get_path_ext(path) # we don't want to accidentally end up searching binary (or pseudo- # binary) files. this is kind of a hack if b.lines and len(b.lines[0]) < 4096: firstline = b.lines[0] else: firstline = '' if path in a.mode_paths: mode_name = a.mode_paths[path] elif basename in a.mode_basenames: mode_name = a.mode_basenames[basename] elif ext in a.mode_extensions: mode_name = a.mode_extensions[ext] elif regex.auto_mode_emacs.search(firstline): mode_name = regex.auto_mode_emacs.search(firstline).group(1) elif regex.auto_mode_vi.search(firstline): mode_name = regex.auto_mode_vi.search(firstline).group(1) else: for (r, name) in a.mode_detection.items(): if r.match(b.lines[0]): mode_name = name break cls = a.modes.get(mode_name, a.modes['fundamental']) self.set_mode(cls(self)) b.add_window(self) # private method used in window constructor def _get_path_ext(self, path): name = os.path.basename(path).lower() tokens = name.split('.') if len(tokens) > 2 and tokens[-1] in ('gz', 'in', 'zip'): return '.%s.%s' % (tokens[-2], tokens[-1]) else: return os.path.splitext(path)[1].lower() # some useful pass-through to application def set_msg(self, s): self.application.set_msg(s) def set_error(self, s): self.application.set_error(s) def clear_error(self): self.application.clear_error() # mode stuff def set_mode(self, m): self.mode = m if m.name not in self.buffer.highlights and m.lexer is not None: self.buffer.highlights[m.name] = highlight.Highlighter(m.lexer) self.buffer.highlights[m.name].highlight(self.buffer.lines) #self.redraw() def get_highlighter(self): if self.mode.lexer is None: return None else: return self.buffer.highlights[self.mode.name] # this is used to temporarily draw the user's attention to another point def set_active_point(self, p, msg='marking on line %(y)d, char %(x)d'): self.active_point = p if not self.point_is_visible(p): self.application.set_error(msg % {'x': p.x, 'y': p.y}) # point left def point_left(self, p): if p.y == 0 and p.x == 0: return None elif p.x == 0: return Point(len(self.buffer.lines[p.y - 1]), p.y - 1) else: return Point(p.x - 1, p.y) # point right def point_right(self, p): blen = len(self.buffer.lines) if p.y >= blen: return None elif p.y == blen-1 and p.x >= len(self.buffer.lines[-1]): return None elif p.x == len(self.buffer.lines[p.y]): return Point(0, p.y + 1) else: return Point(p.x + 1, p.y) # cursors def logical_cursor(self): if len(self.buffer.lines) > self.cursor.y: l = len(self.buffer.lines[self.cursor.y]) else: l = 0 x = min(self.cursor.x, l) return Point(x, self.cursor.y) # last visible point def _calc_last(self): # POSSIBLE BUG (x, y) = self.first.xy() count = 0 while count < self.height - 1 and y < len(self.buffer.lines) - 1: line = self.buffer.lines[y] if x >= len(line) or len(line[x:]) <= self.width: x = 0 y += 1 count += 1 else: count += 1 x += self.width if y < len(self.buffer.lines): x = min(x + self.width, len(self.buffer.lines[y])) self.last = Point(x, y) # redrawing def redraw(self): self._calc_last() def set_size(self, width, height): assert type(width) == type(0), width assert type(height) == type(0), height self.width = width - self.mode.lmargin - self.mode.rmargin self.height = height - self.mode.header - self.mode.footer self.redraw() # region added def region_added(self, p, newlines): (x, y) = self.logical_cursor().xy() l = len(newlines) assert l > 0, repr(newlines) visible = self.point_is_visible(p) if l > 1: if y > p.y: self.cursor = Point(x, y + l - 1) elif y == p.y and x >= p.x: self.cursor = Point(len(newlines[-1]) + x - p.x, y + l - 1) elif y == p.y and x >= p.x: self.cursor = Point(x + len(newlines[0]), y) if not visible and l > 1 and self.first.y > p.y: self.first = Point(self.first.x, self.first.y + l - 1) self.redraw() self.mode.region_added(p, newlines) self.assure_visible_cursor() # region removed def region_removed(self, p1, p2): cursor = self.logical_cursor() (x, y) = cursor.xy() visible = self.point_is_visible(p2) xdelta = p2.x - p1.x ydelta = p2.y - p1.y if cursor < p1: pass elif cursor < p2: self.cursor = p1 elif cursor.y == p2.y: #self.cursor = Point(self.cursor.x - p2.x + p1.x, p1.y) self.cursor = Point(self.cursor.x - xdelta, p1.y) else: #self.cursor = Point(self.cursor.x, self.cursor.y - p2.y + p1.y) self.cursor = Point(self.cursor.x, self.cursor.y - ydelta) if not visible and ydelta and self.first.y > p2.y: self.first = Point(self.first.x, self.first.y - ydelta) self.redraw() self.mode.region_removed(p1, p2) self.assure_visible_cursor() def point_is_visible(self, p): return self.first <= p and p <= self.last def cursor_is_visible(self): return self.point_is_visible(self.logical_cursor()) def first_is_visible(self): return self.point_is_visible(self.buffer.get_buffer_start()) def last_is_visible(self): return self.point_is_visible(self.buffer.get_buffer_end()) def center_view(self): x, y = self.logical_cursor().xy() counter = 0 while counter < self.height / 2: if x > self.width: x -= self.width elif y > 0: y -= 1 x = len(self.buffer.lines[y]) else: x, y = 0, 0 break counter += 1 # trim off the slop x = x - (x % self.width) # in this case, the line is a multiple of width, and there is nothing # (besides a newline) to display on the "next" line, so we should # reduce the wrapping by one line. if x >= self.width and x == len(self.buffer.lines[y]): x -= self.width self.first = Point(x, y) self.redraw() def lower_view(self): (x, y) = self.logical_cursor().xy() counter = 0 while counter < self.height - 1: if x > self.width: x -= self.width elif y > 0: y -= 1 x = len(self.buffer.lines[y]) else: (x, y) = (0, 0) break counter += 1 self.first = Point(x - (x % self.width), y) self.redraw() def upper_view(self): (x, y) = self.logical_cursor().xy() counter = 0 while counter < 2: if x > self.width: x -= self.width elif y > 0: y -= 1 x = len(self.buffer.lines[y]) else: (x, y) = (0, 0) break counter += 1 self.first = Point(x - (x % self.width), y) self.redraw() def assure_visible_cursor(self): if not self.cursor_is_visible(): self.center_view() # moving in buffer def forward(self): self.cursor = self.buffer.forward(self.logical_cursor()) self.assure_visible_cursor() def backward(self): self.cursor = self.buffer.backward(self.logical_cursor()) self.assure_visible_cursor() def end_of_line(self): self.cursor = self.buffer.end_of_line(self.logical_cursor()) self.assure_visible_cursor() def start_of_line(self): self.cursor = self.buffer.start_of_line(self.logical_cursor()) self.assure_visible_cursor() def previous_line(self): self.cursor = self.buffer.previous_line(self.cursor) self.assure_visible_cursor() def next_line(self): self.cursor = self.buffer.next_line(self.cursor) self.assure_visible_cursor() # word handling def find_left_word(self, p=None): if p is None: (x, y) = self.logical_cursor().xy() else: (x, y) = p.xy() start = self.buffer.get_buffer_start() if (x, y) == start: return elif x == 0: y -= 1 x = len(self.buffer.lines[y]) else: x -= 1 while (y, x) >= start and self.xy_char(x, y) not in self.mode.word_letters: if x == 0: y -= 1 x = len(self.buffer.lines[y]) else: x -= 1 found_word = False while (y, x) >= start and self.xy_char(x, y) in self.mode.word_letters: found_word = True if x == 0: y -= 1 x = len(self.buffer.lines[y]) else: x -= 1 if not found_word: return None elif x == len(self.buffer.lines[y]): x = 0 y += 1 else: x += 1 return Point(x, y) def find_right_word(self, p=None): if p is None: (x, y) = self.logical_cursor().xy() else: (x, y) = p.xy() end = self.buffer.get_buffer_end() while (y, x) < end and self.xy_char(x, y) not in self.mode.word_letters: if x == len(self.buffer.lines[y]): x = 0 y += 1 else: x += 1 while (y, x) < end and self.xy_char(x, y) in self.mode.word_letters: if x == len(self.buffer.lines[y]): x = 0 y += 1 else: x += 1 return Point(x, y) def left_word(self): p = self.find_left_word() if p is not None: self.goto(p) def right_word(self): p = self.find_right_word() if p is not None: self.goto(p) def get_word_bounds_at_point(self, p): wl = self.mode.word_letters if len(self.buffer.lines[p.y]) == 0: return None elif self.cursor_char() not in wl: return None x1 = x2 = p.x while x1 > 0 and self.xy_char(x1 - 1, p.y) in wl: x1 -= 1 while x2 < len(self.buffer.lines[p.y]) and self.xy_char(x2, p.y) in wl: x2 += 1 return (Point(x1, p.y), Point(x2, p.y)) def get_word_at_point(self, p): bounds = self.get_word_bounds_at_point(p) if bounds is None: return None else: return self.buffer.get_substring(bounds[0], bounds[1]) def get_word_bounds(self): return self.get_word_bounds_at_point(self.logical_cursor()) def get_word(self): return self.get_word_at_point(self.logical_cursor()) # page up/down def _pshift_up(self, p, num): (x, y) = p.xy() orig_x = x counter = 0 while counter < num and y > 0: if x > self.width: x -= self.width else: y -= 1 x = len(self.buffer.lines[y]) counter += 1 return Point(orig_x, y) def _pshift_down(self, p, num): (x, y) = p.xy() orig_x = x counter = 0 while counter < num and y < len(self.buffer.lines): if x + self.width >= len(self.buffer.lines[y]): y += 1 x = 0 else: x += self.width counter += 1 if y == len(self.buffer.lines): y -= 1 x = len(self.buffer.lines[y]) return Point(orig_x, y) def page_up(self): first_point = self.buffer.get_buffer_start() if self.point_is_visible(first_point): self.goto_beginning() return self.cursor = self._pshift_up(self.cursor, self.height - 3) if self.first > first_point: self.first = self._pshift_up(self.first, self.height - 3) self.redraw() def page_down(self): last_point = self.buffer.get_buffer_end() if self.point_is_visible(last_point): self.goto_end() return self.cursor = self._pshift_down(self.cursor, self.height - 3) if self.last < last_point: self.first = self._pshift_down(self.first, self.height - 3) self.redraw() # jumping in buffer def goto(self, p): self.cursor = p self.assure_visible_cursor() def goto_line(self, n): assert n > 0 and n <= len(self.buffer.lines) , "illegal line: %d" % n self.cursor = Point(0, n - 1) self.assure_visible_cursor() def forward_lines(self, n): assert n > 0, "illegal number of lines: %d" % n y = min(self.logical_cursor().y + n, len(self.buffer.lines) - 1) self.goto(Point(0, y)) def forward_chars(self, n): (x, y) = self.logical_cursor().xy() for _ in xrange(0, n): if x == len(self.buffer.lines[y]): y += 1 x = 0 if y >= len(self.buffer.lines): break else: x += 1 self.goto(Point(x, y)) def goto_char(self, n): self.goto_beginning() self.forward_chars(n) def goto_beginning(self): self.cursor = Point(0, 0) self.assure_visible_cursor() def goto_end(self, force=False): self.cursor = self.buffer.get_buffer_end() (x, y) = self.last_visible_coords() if force or not self.cursor_is_visible(): self.first = Point(x - (x % self.width), y) self.redraw() # TODO: multi-byte characters!!! def point_byte_offset(self, p): n = 0 for i in xrange(0, p.y): n += len(self.buffer.lines[i]) + 1 return n + p.x def cursor_byte_offset(self): return self.point_byte_offset(self.logical_cursor()) def last_visible_coords(self): (x, y) = self.buffer.get_buffer_end().xy() counter = 0 limit = self.height - 1 - CONTEXT while counter < limit: if x > self.width: d = x % self.width if d: x -= d else: x -= self.width elif y > 0: y -= 1 x = len(self.buffer.lines[y]) else: (x, y) = (0, 0) break counter += 1 return (x, y) # mark manipulation def set_mark_point(self, p): self.mark = p def set_mark(self): self.set_mark_point(self.logical_cursor()) self.application.set_error("Mark set") def goto_mark(self): self.goto(self.mark) def switch_mark(self): if self.mark: p = self.mark self.set_mark_point(self.logical_cursor()) self.goto(p) # finding (used by deletion, killing, and copying) def get_line(self): (x, y) = self.logical_cursor().xy() lines = self.buffer.lines if y < len(lines) - 1: return (Point(0, y), Point(0, y + 1)) elif y > 0: return (Point(len(lines[y - 1]), y - 1), Point(len(lines[y]), y)) else: return (None, None) def get_line_left(self): cursor = self.logical_cursor() (x, y) = cursor.xy() lines = self.buffer.lines if (x > 0) and not regex.whitespace.match(lines[y][:x]): return (Point(0, y), cursor) elif y > 0: return (Point(len(lines[y - 1]), y - 1), cursor) else: return (None, None) def get_line_right(self): cursor = self.logical_cursor() (x, y) = cursor.xy() lines = self.buffer.lines if (x < len(lines[y]) and not regex.whitespace.match(lines[y][x:])): return (cursor, Point(len(lines[y]), y)) elif y < len(lines) - 1: return (cursor, Point(0, y + 1)) else: return (None, None) def get_region(self): cursor = self.logical_cursor() p1, p2 = None, None if cursor < self.mark: p1 = cursor p2 = self.mark elif self.mark < cursor: p1 = self.mark p2 = cursor return (p1, p2) def get_left_word(self): p1 = self.find_left_word() p2 = self.logical_cursor() if p1 == p2: return (None, None) else: return (p1, p2) def get_right_word(self): p1 = self.logical_cursor() p2 = self.find_right_word() if p1 == p2: return (None, None) else: return (p1, p2) # deletion def delete_char(self, p): self.buffer.delete_char(p) def delete(self, p1, p2): self.buffer.delete(p1, p2) def delete_line(self): (p1, p2) = self.get_line_right() if p1 is not None: self.delete(p1, p2) def delete_region(self): (p1, p2) = self.get_region() if p1 is not None: self.delete(p1, p2) def delete_left_word(self): (p1, p2) = self.get_left_word() if p1 is not None: self.delete(p1, p2) def delete_right_word(self): (p1, p2) = self.get_right_word() if p1 is not None: self.delete(p1, p2) def left_delete(self): self.buffer.left_delete(self.logical_cursor()) def right_delete(self): self.buffer.right_delete(self.logical_cursor()) # killing def kill(self, p1, p2): killed = self.buffer.get_substring(p1, p2) self.buffer.delete(p1, p2) self.application.push_kill(killed) return killed def kill_line(self): (p1, p2) = self.get_line_right() if p1 is not None: self.kill(p1, p2) def kill_region(self): (p1, p2) = self.get_region() if p1 is not None: self.kill(p1, p2) def kill_left_word(self): (p1, p2) = self.get_left_word() if p1 is not None: self.kill(p1, p2) def kill_right_word(self): (p1, p2) = self.get_right_word() if p1 is not None: self.kill(p1, p2) # copying def copy(self, p1, p2): copied = self.buffer.get_substring(p1, p2) self.application.push_kill(copied) return copied def copy_line(self): (p1, p2) = self.get_line_right() if p1 is not None: return self.copy(p1, p2) def copy_region(self): (p1, p2) = self.get_region() if p1 is not None: return self.copy(p1, p2) # overwriting def overwrite_char_at_cursor(self, c): self.overwrite_char(self.logical_cursor(), c) def overwrite_char(self, p, c): line = self.buffer.lines[p.y] if p.x >= len(line): return elif p.x == len(line) - 1: self.buffer.overwrite_char(p, c) if p.y < len(self.buffer.lines): self.cursor = Point(0, p.y + 1) else: self.buffer.overwrite_char(p, c) self.cursor = Point(p.x + 1, p.y) def replace_with_string(self, p1, p2, s): self.buffer.delete(p1, p2) self.insert_string(p1, s) def replace_with_lines(self, p1, p2, lines): self.buffer.delete(p1, p2) self.insert_lines(p1, lines) # insertion def insert_string_at_cursor(self, s): self.insert_string(self.logical_cursor(), s) def insert_string(self, p, s): lines = s.split('\n') self.insert_lines(p, lines) def insert_lines_at_cursor(self, lines): self.insert_lines(self.logical_cursor(), lines) def insert_lines(self, p, lines): self.buffer.insert_lines(p, lines) self.redraw() # yank/pop def yank(self): self.insert_string_at_cursor(self.application.get_kill()) def get_kill(self): return self.application.get_kill() def has_kill(self, i=-1): return self.application.has_kill(i) def pop_kill(self): return self.application.pop_kill() def push_kill(self, s): return self.application.push_kill(s) # querying def cursor_char(self): return self.point_char(self.logical_cursor()) def point_char(self, p): return self.xy_char(p.x, p.y) def xy_char(self, x, y): if x == len(self.buffer.lines[y]): return "\n" else: return self.buffer.lines[y][x] # undo/redo def undo(self): p = self.buffer.undo() if not self.point_is_visible(p): self.goto(p) def redo(self): p = self.buffer.redo() if not self.point_is_visible(p): self.goto(p) # highlighting tokens def get_token_list(self): return self.get_token_list_at_point(self.logical_cursor()) def get_token_list_at_point(self, p): return self.get_highlighter().tokens[p.y] def get_token(self): return self.get_token_at_point(self.logical_cursor()) def get_token2(self): c = self.logical_cursor() p = Point(max(0, c.x - 1), c.y) return self.get_token_at_point(p) def get_token_at_point(self, p): for token in self.get_highlighter().tokens[p.y]: if token.end_x() <= p.x: continue elif token.x > p.x: continue else: return token return None def get_next_token_by_lambda(self, p, f): tokens = self.get_highlighter().tokens[p.y] for token in tokens: if token.x < p.x: continue if f(token): return token return None def get_next_token_by_type(self, p, name): return self.get_next_token_by_lambda(p, lambda t: t.name == name) def get_next_token_except_type(self, p, name): return self.get_next_token_by_lambda(p, lambda t: t.name != name) def get_next_token_by_type_regex(self, p, name, regex): l = lambda t: t.name == name and regex.match(t.string) return self.get_next_token_by_lambda(p, l) def get_next_token_except_type_regex(self, p, name, regex): l = lambda t: t.name != name or regex.match(t.string) return self.get_next_token_by_lambda(p, l) def get_next_token_by_types(self, p, *names): return self.get_next_token_by_lambda(p, lambda t: t.name in names) def get_next_token_except_types(self, p, *names): return self.get_next_token_by_lambda(p, lambda t: t.name not in names) # application drawing # # render methods return a list of lists of RenderString objects # (i.e. multiple physical lines containing multiple areas to be drawn) def render_line(self, y, width): if self.mode.name in self.buffer.highlights: return self.render_line_lit(y, width) else: return self.render_line_raw(y, width) def render_line_raw(self, y, width): if y >= len(self.buffer.lines): r = RenderString(s='~', attrs=color.build('red', 'default')) return [(r,)] x = 0 line = self.buffer.lines[y] lines = [] if line: while x < len(line): r = RenderString(s=line[x:x + width]) lines.append((r,)) x += width else: r = RenderString(s='') lines.append((r,)) return lines def render_line_lit(self, y, width): if y >= len(self.buffer.lines): r = RenderString(s='~', attrs=color.build('red', 'default')) return [(r,)] highlighter = self.buffer.highlights[self.mode.name] line = [] lines = [] j = x = 0 while j < len(highlighter.tokens[y]): # get our token, and do some basic checking/bleaching token = highlighter.tokens[y][j] if token.string.endswith('\n'): tstring = token.string[:-1] else: tstring = token.string assert token.y == y, '%d == %d' % (token.y, y) # figure out what portion of the string to use s_offset = max(x - token.x, 0) s = tstring[s_offset:] # for debugging things like lexing/relexing/etc. if token._debug: attr = color.build('blue', 'green', 'bold') elif token.color: attr = color.build(*token.color) else: attr = color.build("default", "default") # ok, so add a region with data, position, and color info x_offset = max(token.x - x, 0) r = RenderString(s=s[:width - x_offset], x=x_offset, attrs=attr) line.append(r) # see if the token is wrapping, or if we move on to the next one if x_offset + len(s) > width: lines.append(line) line = [] x += width else: j += 1 lines.append(line) return lines