#!/usr/bin/env python # -*- coding: utf-8 -*- """ Middleware layer that communicates between editor and Zen Coding. This layer describes all available Zen Coding actions, like "Expand Abbreviation". @author Sergey Chikuyonok (serge.che@gmail.com) @link http://chikuyonok.ru """ from zencoding import zen_core as zen_coding from zencoding import html_matcher import re def find_abbreviation(editor): """ Search for abbreviation in editor from current caret position @param editor: Editor instance @type editor: ZenEditor @return: str """ start, end = editor.get_selection_range() if start != end: # abbreviation is selected by user return editor.get_content()[start, end]; # search for new abbreviation from current caret position cur_line_start, cur_line_end = editor.get_current_line_range() return zen_coding.extract_abbreviation(editor.get_content()[cur_line_start:start]) def expand_abbreviation(editor, syntax=None, profile_name=None): """ Find from current caret position and expand abbreviation in editor @param editor: Editor instance @type editor: ZenEditor @param syntax: Syntax type (html, css, etc.) @type syntax: str @param profile_name: Output profile name (html, xml, xhtml) @type profile_name: str @return: True if abbreviation was expanded successfully """ if syntax is None: syntax = editor.get_syntax() if profile_name is None: profile_name = editor.get_profile_name() range_start, caret_pos = editor.get_selection_range() abbr = find_abbreviation(editor) content = '' if abbr: content = zen_coding.expand_abbreviation(abbr, syntax, profile_name) if content: editor.replace_content(content, caret_pos - len(abbr), caret_pos) return True return False def expand_abbreviation_with_tab(editor, syntax, profile_name='xhtml'): """ A special version of expandAbbreviation function: if it can't find abbreviation, it will place Tab character at caret position @param editor: Editor instance @type editor: ZenEditor @param syntax: Syntax type (html, css, etc.) @type syntax: str @param profile_name: Output profile name (html, xml, xhtml) @type profile_name: str """ if not expand_abbreviation(editor, syntax, profile_name): editor.replace_content(zen_coding.get_variable('indentation'), editor.get_caret_pos()) return True def match_pair(editor, direction='out', syntax=None): """ Find and select HTML tag pair @param editor: Editor instance @type editor: ZenEditor @param direction: Direction of pair matching: 'in' or 'out'. @type direction: str """ direction = direction.lower() if syntax is None: syntax = editor.get_profile_name() range_start, range_end = editor.get_selection_range() cursor = range_end content = editor.get_content() rng = None old_open_tag = html_matcher.last_match['opening_tag'] old_close_tag = html_matcher.last_match['closing_tag'] if direction == 'in' and old_open_tag and range_start != range_end: # user has previously selected tag and wants to move inward if not old_close_tag: # unary tag was selected, can't move inward return False elif old_open_tag.start == range_start: if content[old_open_tag.end] == '<': # test if the first inward tag matches the entire parent tag's content _r = html_matcher.find(content, old_open_tag.end + 1, syntax) if _r[0] == old_open_tag.end and _r[1] == old_close_tag.start: rng = html_matcher.match(content, old_open_tag.end + 1, syntax) else: rng = (old_open_tag.end, old_close_tag.start) else: rng = (old_open_tag.end, old_close_tag.start) else: new_cursor = content[0:old_close_tag.start].find('<', old_open_tag.end) search_pos = new_cursor + 1 if new_cursor != -1 else old_open_tag.end rng = html_matcher.match(content, search_pos, syntax) else: rng = html_matcher.match(content, cursor, syntax) if rng and rng[0] is not None: editor.create_selection(rng[0], rng[1]) return True else: return False def match_pair_inward(editor): return match_pair(editor, 'in') def match_pair_outward(editor): return match_pair(editor, 'out') def narrow_to_non_space(text, start, end): """ Narrow down text indexes, adjusting selection to non-space characters @type text: str @type start: int @type end: int @return: list """ # narrow down selection until first non-space character while start < end: if not text[start].isspace(): break start += 1 while end > start: end -= 1 if not text[end].isspace(): end += 1 break return start, end def wrap_with_abbreviation(editor, abbr, syntax=None, profile_name=None): """ Wraps content with abbreviation @param editor: Editor instance @type editor: ZenEditor @param syntax: Syntax type (html, css, etc.) @type syntax: str @param profile_name: Output profile name (html, xml, xhtml) @type profile_name: str """ if not abbr: return None if syntax is None: syntax = editor.get_syntax() if profile_name is None: profile_name = editor.get_profile_name() start_offset, end_offset = editor.get_selection_range() content = editor.get_content() if start_offset == end_offset: # no selection, find tag pair rng = html_matcher.match(content, start_offset, profile_name) if rng[0] is None: # nothing to wrap return None else: start_offset, end_offset = rng start_offset, end_offset = narrow_to_non_space(content, start_offset, end_offset) line_bounds = get_line_bounds(content, start_offset) padding = get_line_padding(content[line_bounds[0]:line_bounds[1]]) new_content = content[start_offset:end_offset] result = zen_coding.wrap_with_abbreviation(abbr, unindent_text(new_content, padding), syntax, profile_name) if result: editor.replace_content(result, start_offset, end_offset) return True return False def unindent(editor, text): """ Unindent content, thus preparing text for tag wrapping @param editor: Editor instance @type editor: ZenEditor @param text: str @return str """ return unindent_text(text, get_current_line_padding(editor)) def unindent_text(text, pad): """ Removes padding at the beginning of each text's line @type text: str @type pad: str """ lines = zen_coding.split_by_lines(text) for i,line in enumerate(lines): if line.startswith(pad): lines[i] = line[len(pad):] return zen_coding.get_newline().join(lines) def get_current_line_padding(editor): """ Returns padding of current editor's line @return str """ return get_line_padding(editor.get_current_line()) def get_line_padding(line): """ Returns padding of current editor's line @return str """ m = re.match(r'^(\s+)', line) return m and m.group(0) or '' def find_new_edit_point(editor, inc=1, offset=0): """ Search for new caret insertion point @param editor: Editor instance @type editor: ZenEditor @param inc: Search increment: -1 — search left, 1 — search right @param offset: Initial offset relative to current caret position @return: -1 if insertion point wasn't found """ cur_point = editor.get_caret_pos() + offset content = editor.get_content() max_len = len(content) next_point = -1 re_empty_line = r'^\s+$' def get_line(ix): start = ix while start >= 0: c = content[start] if c == '\n' or c == '\r': break start -= 1 return content[start:ix] while cur_point < max_len and cur_point > 0: cur_point += inc cur_char = content[cur_point] next_char = content[cur_point + 1] prev_char = content[cur_point - 1] if cur_char in '"\'': if next_char == cur_char and prev_char == '=': # empty attribute next_point = cur_point + 1 elif cur_char == '>' and next_char == '<': # between tags next_point = cur_point + 1 elif cur_char in '\r\n': # empty line if re.search(re_empty_line, get_line(cur_point - 1)): next_point = cur_point if next_point != -1: break return next_point def prev_edit_point(editor): """ Move caret to previous edit point @param editor: Editor instance @type editor: ZenEditor """ cur_pos = editor.get_caret_pos() new_point = find_new_edit_point(editor, -1) if new_point == cur_pos: # we're still in the same point, try searching from the other place new_point = find_new_edit_point(editor, -1, -2) if new_point != -1: editor.set_caret_pos(new_point) return True return False def next_edit_point(editor): """ Move caret to next edit point @param editor: Editor instance @type editor: ZenEditor """ new_point = find_new_edit_point(editor, 1) if new_point != -1: editor.set_caret_pos(new_point) return True return False def insert_formatted_newline(editor, mode='html'): """ Inserts newline character with proper indentation @param editor: Editor instance @type editor: ZenEditor @param mode: Syntax mode (only 'html' is implemented) @type mode: str """ caret_pos = editor.get_caret_pos() nl = zen_coding.get_newline() pad = zen_coding.get_variable('indentation') if mode == 'html': # let's see if we're breaking newly created tag pair = html_matcher.get_tags(editor.get_content(), editor.get_caret_pos(), editor.get_profile_name()) if pair[0] and pair[1] and pair[0]['type'] == 'tag' and pair[0]['end'] == caret_pos and pair[1]['start'] == caret_pos: editor.replace_content(nl + pad + zen_coding.get_caret_placeholder() + nl, caret_pos) else: editor.replace_content(nl, caret_pos) else: editor.replace_content(nl, caret_pos) return True def select_line(editor): """ Select line under cursor @param editor: Editor instance @type editor: ZenEditor """ start, end = editor.get_current_line_range(); editor.create_selection(start, end) return True def go_to_matching_pair(editor): """ Moves caret to matching opening or closing tag @param editor: Editor instance @type editor: ZenEditor """ content = editor.get_content() caret_pos = editor.get_caret_pos() if content[caret_pos] == '<': # looks like caret is outside of tag pair caret_pos += 1 tags = html_matcher.get_tags(content, caret_pos, editor.get_profile_name()) if tags and tags[0]: # match found open_tag, close_tag = tags if close_tag: # exclude unary tags if open_tag['start'] <= caret_pos and open_tag['end'] >= caret_pos: editor.set_caret_pos(close_tag['start']) elif close_tag['start'] <= caret_pos and close_tag['end'] >= caret_pos: editor.set_caret_pos(open_tag['start']) return True return False def merge_lines(editor): """ Merge lines spanned by user selection. If there's no selection, tries to find matching tags and use them as selection @param editor: Editor instance @type editor: ZenEditor """ start, end = editor.get_selection_range() if start == end: # find matching tag pair = html_matcher.match(editor.get_content(), editor.get_caret_pos(), editor.get_profile_name()) if pair and pair[0] is not None: start, end = pair if start != end: # got range, merge lines text = editor.get_content()[start:end] lines = map(lambda s: re.sub(r'^\s+', '', s), zen_coding.split_by_lines(text)) text = re.sub(r'\s{2,}', ' ', ''.join(lines)) editor.replace_content(text, start, end) editor.create_selection(start, start + len(text)) return True return False def toggle_comment(editor): """ Toggle comment on current editor's selection or HTML tag/CSS rule @type editor: ZenEditor """ syntax = editor.get_syntax() if syntax == 'css': return toggle_css_comment(editor) else: return toggle_html_comment(editor) def toggle_html_comment(editor): """ Toggle HTML comment on current selection or tag @type editor: ZenEditor @return: True if comment was toggled """ start, end = editor.get_selection_range() content = editor.get_content() if start == end: # no selection, find matching tag pair = html_matcher.get_tags(content, editor.get_caret_pos(), editor.get_profile_name()) if pair and pair[0]: # found pair start = pair[0].start end = pair[1] and pair[1].end or pair[0].end return generic_comment_toggle(editor, '', start, end) def toggle_css_comment(editor): """ Simple CSS commenting @type editor: ZenEditor @return: True if comment was toggled """ start, end = editor.get_selection_range() if start == end: # no selection, get current line start, end = editor.get_current_line_range() # adjust start index till first non-space character start, end = narrow_to_non_space(editor.get_content(), start, end) return generic_comment_toggle(editor, '/*', '*/', start, end) def search_comment(text, pos, start_token, end_token): """ Search for nearest comment in str, starting from index from @param text: Where to search @type text: str @param pos: Search start index @type pos: int @param start_token: Comment start string @type start_token: str @param end_token: Comment end string @type end_token: str @return: None if comment wasn't found, list otherwise """ start_ch = start_token[0] end_ch = end_token[0] comment_start = -1 comment_end = -1 def has_match(tx, start): return text[start:start + len(tx)] == tx # search for comment start while pos: pos -= 1 if text[pos] == start_ch and has_match(start_token, pos): comment_start = pos break if comment_start != -1: # search for comment end pos = comment_start content_len = len(text) while content_len >= pos: pos += 1 if text[pos] == end_ch and has_match(end_token, pos): comment_end = pos + len(end_token) break if comment_start != -1 and comment_end != -1: return comment_start, comment_end else: return None def generic_comment_toggle(editor, comment_start, comment_end, range_start, range_end): """ Generic comment toggling routine @type editor: ZenEditor @param comment_start: Comment start token @type comment_start: str @param comment_end: Comment end token @type comment_end: str @param range_start: Start selection range @type range_start: int @param range_end: End selection range @type range_end: int @return: bool """ content = editor.get_content() caret_pos = [editor.get_caret_pos()] new_content = None def adjust_caret_pos(m): caret_pos[0] -= len(m.group(0)) return '' def remove_comment(text): """ Remove comment markers from string @param {Sting} str @return {String} """ text = re.sub(r'^' + re.escape(comment_start) + r'\s*', adjust_caret_pos, text) return re.sub(r'\s*' + re.escape(comment_end) + '$', '', text) def has_match(tx, start): return content[start:start + len(tx)] == tx # first, we need to make sure that this substring is not inside comment comment_range = search_comment(content, caret_pos[0], comment_start, comment_end) if comment_range and comment_range[0] <= range_start and comment_range[1] >= range_end: # we're inside comment, remove it range_start, range_end = comment_range new_content = remove_comment(content[range_start:range_end]) else: # should add comment # make sure that there's no comment inside selection new_content = '%s %s %s' % (comment_start, re.sub(re.escape(comment_start) + r'\s*|\s*' + re.escape(comment_end), '', content[range_start:range_end]), comment_end) # adjust caret position caret_pos[0] += len(comment_start) + 1 # replace editor content if new_content is not None: d = caret_pos[0] - range_start new_content = new_content[0:d] + zen_coding.get_caret_placeholder() + new_content[d:] editor.replace_content(unindent(editor, new_content), range_start, range_end) return True return False def split_join_tag(editor, profile_name=None): """ Splits or joins tag, e.g. transforms it into a short notation and vice versa:
: join
: split @param editor: Editor instance @type editor: ZenEditor @param profile_name: Profile name @type profile_name: str """ caret_pos = editor.get_caret_pos() profile = zen_coding.get_profile(profile_name or editor.get_profile_name()) caret = zen_coding.get_caret_placeholder() # find tag at current position pair = html_matcher.get_tags(editor.get_content(), caret_pos, profile_name or editor.get_profile_name()) if pair and pair[0]: new_content = pair[0].full_tag if pair[1]: # join tag closing_slash = '' if profile['self_closing_tag'] is True: closing_slash = '/' elif profile['self_closing_tag'] == 'xhtml': closing_slash = ' /' new_content = re.sub(r'\s*>$', closing_slash + '>', new_content) # add caret placeholder if len(new_content) + pair[0].start < caret_pos: new_content += caret else: d = caret_pos - pair[0].start new_content = new_content[0:d] + caret + new_content[d:] editor.replace_content(new_content, pair[0].start, pair[1].end) else: # split tag nl = zen_coding.get_newline() pad = zen_coding.get_variable('indentation') # define tag content depending on profile tag_content = profile['tag_nl'] is True and nl + pad + caret + nl or caret new_content = '%s%s' % (re.sub(r'\s*\/>$', '>', new_content), tag_content, pair[0].name) editor.replace_content(new_content, pair[0].start, pair[0].end) return True else: return False def get_line_bounds(text, pos): """ Returns line bounds for specific character position @type text: str @param pos: Where to start searching @type pos: int @return: list """ start = 0 end = len(text) - 1 # search left for i in range(pos - 1, 0, -1): if text[i] in '\n\r': start = i + 1 break # search right for i in range(pos, len(text)): if text[i] in '\n\r': end = i break return start, end def remove_tag(editor): """ Gracefully removes tag under cursor @type editor: ZenEditor """ caret_pos = editor.get_caret_pos() content = editor.get_content() # search for tag pair = html_matcher.get_tags(content, caret_pos, editor.get_profile_name()) if pair and pair[0]: if not pair[1]: # simply remove unary tag editor.replace_content(zen_coding.get_caret_placeholder(), pair[0].start, pair[0].end) else: tag_content_range = narrow_to_non_space(content, pair[0].end, pair[1].start) start_line_bounds = get_line_bounds(content, tag_content_range[0]) start_line_pad = get_line_padding(content[start_line_bounds[0]:start_line_bounds[1]]) tag_content = content[tag_content_range[0]:tag_content_range[1]] tag_content = unindent_text(tag_content, start_line_pad) editor.replace_content(zen_coding.get_caret_placeholder() + tag_content, pair[0].start, pair[1].end) return True else: return False