import os.path, string import color, highlight, regex from point import Point from render import RenderString WORD_LETTERS = list(string.letters + string.digits) # 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 on # the shorter line will "set" the cursor to its position on the shorter line. class Window(object): margins = ((80, 'blue'),) 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 = "" self.hidden_ranges = {} self.hidden_lines = {} 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) if path in self.application.mode_paths: mode_name = self.application.mode_paths[path] elif basename in self.application.mode_basenames: mode_name = self.application.mode_basenames[basename] elif ext in self.application.mode_extensions: mode_name = self.application.mode_extensions[ext] elif len(b.lines) > 0 and \ b.lines[0].startswith('#!'): line = b.lines[0] for word in self.application.mode_detection: if word in line: mode_name = self.application.mode_detection[word] if mode_name is None: mode_name = "fundamental" m = self.application.modes[mode_name](self) self.set_mode(m) 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_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 modename = m.name() if modename not in self.buffer.highlights and m.lexer is not None: self.buffer.highlights[modename] = highlight.Highlighter(m.lexer) self.buffer.highlights[modename].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.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 self.first = Point(x - (x % self.width), 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): p = self.logical_cursor() if self.first > p: self.upper_view() elif p > self.last: self.lower_view() # moving in buffer def forward(self): cursor = self.logical_cursor() if cursor.x < len(self.buffer.lines[cursor.y]): self.cursor = Point(cursor.x + 1, cursor.y) elif cursor.y < len(self.buffer.lines) -1: self.cursor = Point(0, cursor.y + 1) self.assure_visible_cursor() def backward(self): cursor = self.logical_cursor() if cursor.x > 0: self.cursor = Point(cursor.x - 1, cursor.y) elif cursor.y > 0: x = len(self.buffer.lines[cursor.y - 1]) self.cursor = Point(x, cursor.y - 1) self.assure_visible_cursor() def end_of_line(self): cursor = self.logical_cursor() self.cursor = Point(len(self.buffer.lines[cursor.y]), cursor.y) self.assure_visible_cursor() def start_of_line(self): cursor = self.logical_cursor() self.cursor = Point(0, cursor.y) self.assure_visible_cursor() def previous_line(self): if self.cursor.y > 0: self.cursor = Point(self.cursor.x, self.cursor.y - 1) self.assure_visible_cursor() def next_line(self): if self.cursor.y < len(self.buffer.lines) - 1: self.cursor = Point(self.cursor.x, self.cursor.y + 1) 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 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 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 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 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=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, wl=WORD_LETTERS): bounds = self.get_word_bounds_at_point(p, wl) if bounds is None: return None else: return self.buffer.get_substring(bounds[0], bounds[1]) def get_word_bounds(self, wl=WORD_LETTERS): return self.get_word_bounds_at_point(self.logical_cursor(), wl) def get_word(self, wl=WORD_LETTERS): return self.get_word_at_point(self.logical_cursor(), wl) # 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 i in range(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.logical_cursor().xy() if x == 0: y -= 1 x = len(self.buffer.lines[y]) else: x -= 1 counter = 0 while counter < self.height - 3: 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 if force or not self.cursor_is_visible(): self.first = Point(x - (x % self.width), y) self.redraw() # 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) # deletion def left_delete(self): (x, y) = self.logical_cursor().xy() if x > 0: self.buffer.delete_char(Point(x - 1, y)) elif y > 0: x = len(self.buffer.lines[y - 1]) self.buffer.delete_char(Point(x, y - 1)) def right_delete(self): cursor = self.logical_cursor() if cursor < self.last: self.buffer.delete_char(cursor) else: pass # killing def kill_line(self): return self.copy_line(kill=True) def kill_region(self): return self.copy_region(kill=True) def kill_left_word(self): p1 = self.find_left_word() p2 = self.logical_cursor() if p1 == p2: return return self.kill(p1, p2) def kill_right_word(self): p1 = self.logical_cursor() p2 = self.find_right_word() if p1 == p2: return return self.kill(p1, p2) def copy_line(self, kill=False): 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:])): limit = Point(len(lines[y]), y) elif y < len(lines) - 1: limit = Point(0, y + 1) else: return if kill: return self.kill(cursor, limit) else: return self.copy(cursor, limit) def copy_region(self, kill=False): cursor = self.logical_cursor() if cursor < self.mark: p1 = cursor p2 = self.mark elif self.mark < cursor: p1 = self.mark p2 = cursor else: self.input_line = "Empty kill region" return if kill: return self.kill(p1, p2) else: return self.copy(p1, p2) 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 copy(self, p1, p2): copied = self.buffer.get_substring(p1, p2) self.application.push_kill(copied) return copied # 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) # 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] # hiding stuff def hide(self, y1, y2): if y2 == 0: return for (py1, py2) in self.hidden_ranges.itervalues(): if ((y1 >= py1 and y1 < py2) or (y2 >= py1 and y2 < py2) or (y1 <= py1 and y2 >= py2)): return for i in range(y1, y2): self.hidden_lines[i] = y2 self.hidden_ranges[y2] = (y1, y2) def ishidden(self, y): return self.hidden_lines.setdefault(y, False) def hiddenindicator(self, y): return y in self.hidden_ranges def unhide(self, y): py2 = self.ishidden(y) if not py2: return py1 = self.hidden_ranges[py2][0] del self.hidden_ranges[py2] for i in range(py1, py2): self.hidden_lines[i] = False # 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(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): modename = self.mode.name() if modename 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,)] modename = self.mode.name() highlighter = self.buffer.highlights[modename] 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