├── README.md ├── WXML_completions.py ├── WXML_completions.sublime-settings ├── assets └── images │ ├── sublime-setting.png │ ├── wxapp-api.gif │ ├── wxapp-wxml.gif │ ├── wxml-complete-1.gif │ ├── wxml-complete-2.gif │ ├── wxml-complete-3.gif │ ├── wxml-complete-4.gif │ ├── wxml-complete.gif │ ├── wxml-syntax-highlight.png │ └── wxss-syntax-highlight.png ├── docs └── README.zh-ch.md ├── samples ├── test.wxml ├── test.wxs └── test.wxss ├── wxapi ├── app.sublime-snippet ├── check_session.sublime-snippet ├── choose_image.sublime-snippet ├── choose_location.sublime-snippet ├── choose_video.sublime-snippet ├── clear_storage.sublime-snippet ├── clear_storage_sync.sublime-snippet ├── close_socket.sublime-snippet ├── connect_socket.sublime-snippet ├── download_file.sublime-snippet ├── get_background_audio_player_state.sublime-snippet ├── get_image_info.sublime-snippet ├── get_location.sublime-snippet ├── get_network_type.sublime-snippet ├── get_saved_file_info.sublime-snippet ├── get_saved_file_list.sublime-snippet ├── get_storage.sublime-snippet ├── get_storage_info.sublime-snippet ├── get_storage_info_sync.sublime-snippet ├── get_storage_sync.sublime-snippet ├── get_system_info.sublime-snippet ├── get_user_info.sublime-snippet ├── hide_loading.sublime-snippet ├── hide_navbar_loading.sublime-snippet ├── hide_toast.sublime-snippet ├── login.sublime-snippet ├── make_phone_call.sublime-snippet ├── navigate_back.sublime-snippet ├── navigate_to.sublime-snippet ├── on_Accelerometer_change.sublime-snippet ├── on_background_audio_pause.sublime-snippet ├── on_background_audio_play.sublime-snippet ├── on_background_audio_stop.sublime-snippet ├── on_compass_change.sublime-snippet ├── on_socket_close.sublime-snippet ├── on_socket_error.sublime-snippet ├── on_socket_message.sublime-snippet ├── on_socket_open.sublime-snippet ├── open_document.sublime-snippet ├── open_location.sublime-snippet ├── page.sublime-snippet ├── page_ScrollTo.sublime-snippet ├── pause_background_audio.sublime-snippet ├── pause_voice.sublime-snippet ├── play_background_audio.sublime-snippet ├── play_voice.sublime-snippet ├── preview_image.sublime-snippet ├── redirect_to.sublime-snippet ├── remove_saved_file.sublime-snippet ├── remove_storage.sublime-snippet ├── remove_storage_sync.sublime-snippet ├── request.sublime-snippet ├── request_payment.sublime-snippet ├── save_file.sublime-snippet ├── seek_background_audio.sublime-snippet ├── send_socket_message.sublime-snippet ├── set_navbar_title.sublime-snippet ├── set_storage.sublime-snippet ├── set_storage_sync.sublime-snippet ├── show_actionsheet.sublime-snippet ├── show_loading.sublime-snippet ├── show_modal.sublime-snippet ├── show_navbar_loading.sublime-snippet ├── show_toast.sublime-snippet ├── start_record.sublime-snippet ├── stop_background_audio.sublime-snippet ├── stop_record.sublime-snippet ├── stop_voice.sublime-snippet └── upload_file.sublime-snippet ├── wxml ├── Comments.tmPreferences ├── WXML.sublime-settings └── WXML.sublime-syntax ├── wxs └── JavaScript.sublime-settings └── wxss └── CSS.sublime-settings /README.md: -------------------------------------------------------------------------------- 1 | English | [简体中文](./docs/README.zh-ch.md) 2 | 3 | 4 | # Sublime-wxapp 5 | 6 | Sublime Text 3 syntax highlighting and auto completion for `.wxml` file(WeChat 'mini apps'). 7 | 8 | ## Install 9 | 10 | - Package Control 11 | 12 | Search `Sublime wxapp` via Package Control: Install Package 13 | 14 | - Git 15 | 16 | Git clone this repository to Sublime Packages Path. 17 | 18 | - Zip 19 | 20 | Download zip and unzip to Sublime Packages Path. 21 | 22 | ## Setting 23 | 24 | In order to improve WXML completion efficiency,you shuld edit your Preferences.sublime-settings and add config below: 25 | 26 | ```js 27 | "auto_complete_triggers": 28 | [ 29 | { 30 | "characters": "abcdefghijklmnopqrstuvwxyz< :.", 31 | "selector": "text.wxml" 32 | } 33 | ], 34 | ``` 35 | 36 | ![](assets/images/sublime-setting.png) 37 | 38 | ## Feature 39 | 40 | ### `.wxml` file syntax highlighting 41 | 42 | ![](assets/images/wxml-syntax-highlight.png) 43 | 44 | ### `.wxss` file syntax highlighting 45 | 46 | ![](assets/images/wxss-syntax-highlight.png) 47 | 48 | ### WeChat 'mini apps' components auto completion 49 | 50 | 1: Support `view:if`、`view:for`、`view:class`. 51 | 52 | ![](assets/images/wxml-complete-1.gif) 53 | 54 | 2: Support `view.class` and `view#id`. 55 | 56 | ![](assets/images/wxml-complete-2.gif) 57 | 58 | 3: Will complement different content depending on the type of attribute values. 59 | 60 | ![](assets/images/wxml-complete-3.gif) 61 | 62 | 4: Support colon (:) matching. 63 | 64 | ![](assets/images/wxml-complete-4.gif) 65 | 66 | 67 | ### WeChat 'mini apps' apis auto completion 68 | 69 | ![](assets/images/wxapp-api.gif) 70 | -------------------------------------------------------------------------------- /WXML_completions.py: -------------------------------------------------------------------------------- 1 | import sublime, sublime_plugin 2 | import re 3 | 4 | def match(rex, str): 5 | # 返回正则匹配结果 6 | m = rex.match(str) 7 | if m: 8 | return m.group(0) 9 | else: 10 | return None 11 | 12 | 13 | def make_completion(tag): 14 | # 返回标签的completion内容 15 | return (tag + '\tTag', tag + '>$0') 16 | 17 | 18 | class GetSettingsPrepare(): 19 | def init(self): 20 | 21 | # 读取自己的设置文件 22 | settings = sublime.load_settings('WXML_completions.sublime-settings') 23 | # print('settings:', settings) 24 | 25 | # 标签私有属性的配置对象 26 | tag_data = settings.get('tag_data') 27 | 28 | # 标签的全局属性 29 | global_attributes = settings.get('global_attributes') 30 | 31 | # 标签列表 32 | tag_list = list(tag_data.keys()) 33 | 34 | # 自定义标签的completion列表 35 | custom_completion = settings.get('custom_completion') 36 | 37 | # 按标签首字母,对标签的comple列表进行分组 38 | self.prefix_completion_tag_dict = {} 39 | 40 | for tag in tag_list: 41 | prefix = tag[0] 42 | tag_completion = make_completion(tag) 43 | custom_list = [] 44 | 45 | # 将自定义标签的completion列表合并到tag_dict 46 | for item in custom_completion: 47 | tag_name = item[0].split('\t') 48 | custom_tag = tag_name[0] 49 | tag_name = tag_name[0].split(':')[0] 50 | if custom_tag == tag: 51 | tag_completion = 'false' 52 | if tag_name == tag: 53 | custom_list.append(item) 54 | 55 | # 如果自定义的completion为原标签,则覆盖 56 | # 否则还需要将原标签的completion注入到tag_dict 57 | if tag_completion != 'false': 58 | self.prefix_completion_tag_dict.setdefault(prefix, []).append(tag_completion) 59 | self.prefix_completion_tag_dict.setdefault(prefix, []).extend(custom_list) 60 | 61 | # 获取标签对应的属性列表 62 | self.tag_data = tag_data 63 | self.global_attributes = global_attributes 64 | 65 | # 打印测试 66 | # print(self.tag_data) 67 | # print(self.global_attributes) 68 | # print(self.prefix_completion_tag_dict.get('b')) 69 | # print(self.prefix_completion_tag_dict.get('i')) 70 | 71 | 72 | # Sublime Text 3的资源加载都是异步,在试图访问之前使用 plugin_loaded() 的回调 73 | setting_cache = GetSettingsPrepare() 74 | 75 | if int(sublime.version()) < 3000: 76 | setting_cache.init() 77 | else: 78 | def plugin_loaded(): 79 | global setting_cache 80 | setting_cache.init() 81 | 82 | 83 | class TagCompletions(sublime_plugin.EventListener): 84 | global setting_cache 85 | 86 | def on_query_completions(self, view, prefix, locations): 87 | # 该函数当sublime触发自动完成时被执行 88 | # 在该回调函数中可以返回complete列表,用于sublime的自动完成提示 89 | # 90 | # view: 视图对象,提供相关操作函数 91 | # prefix: 当前输入的匹配字符串,匹配字符串为被换行符\n,制表符\t,空格' ',点号'.',冒号':'隔断 92 | # locations: 当前输入的位置,由于可以多行编辑,所以这里是一个数组 93 | 94 | # 仅针对wxml有效 95 | if not view.match_selector(locations[0], "text.wxml"): 96 | return [] 97 | 98 | # 检测当前作用域是否在标签内 99 | is_inside_tag = view.match_selector(locations[0], 100 | "text.wxml meta.tag - text.wxml punctuation.definition.tag.begin") 101 | 102 | # 返回complete列表 103 | return self.get_completions(view, prefix, locations, is_inside_tag) 104 | 105 | def get_completions(self, view, prefix, locations, is_inside_tag): 106 | 107 | # flags-sublime.INHIBIT_WORD_COMPLETIONS 108 | # 抑制文档中的单词生成的completion列表 109 | # 110 | # flags-sublime.INHIBIT_EXPLICIT_COMPLETIONS 111 | # 抑制从.sublime-completions中生成的completion列表 112 | 113 | # 获取ch,表示当前匹配字符串的前一个字符是什么 114 | pt = locations[0] - len(prefix) - 1 115 | ch = view.substr(sublime.Region(pt, pt + 1)) 116 | completion_list = [] 117 | 118 | # 打印测试 119 | # print('get_completions:', {'prefix':prefix, 'locations':locations, 'is_inside_tag':is_inside_tag, 'ch':ch}) 120 | 121 | # 如果不在标签作用域下 122 | # 优先匹配 tag.class 和 tag#id 模式 123 | if not is_inside_tag: 124 | tag_attr_expr = self.expand_tag_attributes(view, locations) 125 | if tag_attr_expr != []: 126 | return (tag_attr_expr, sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS) 127 | 128 | # 如果在标签作用域下,且不以<开头 129 | # 则匹配标签属性 130 | if is_inside_tag and ch != '<': 131 | if ch in [' ', '"', '\t', '\n', ':', '.', '@']: 132 | completion_list = self.get_attribute_completions(view, locations, prefix, ch) 133 | return (completion_list, sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS) 134 | 135 | # 如果是在字符串中,则匹配为空 136 | if prefix == '': 137 | return ([], sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS) 138 | 139 | # 根据匹配字符串的第一个字符列出符合条件的标签列表 140 | completion_list = setting_cache.prefix_completion_tag_dict.get(prefix[0], []) 141 | 142 | # 如果匹配字符串没有<符号,则完成列表需要加上 143 | if ch != '<': 144 | completion_list = [(pair[0], '<' + pair[1]) for pair in completion_list] 145 | 146 | # 如果是在标签作用域下,则需要抑制单词的completion列表以及.sublime-completions列表 147 | # 如果非标签作用域下,但是成功匹配到了标签的completion列表,也需要抑制 148 | flags = 0 149 | if is_inside_tag: 150 | flags = sublime.INHIBIT_WORD_COMPLETIONS | sublime.INHIBIT_EXPLICIT_COMPLETIONS 151 | 152 | # 返回completion列表 153 | return (completion_list, flags) 154 | 155 | def expand_tag_attributes(self, view, locations): 156 | # 支持便捷方式 157 | # tag.class 158 | # tag#id 159 | 160 | # lines返回当前输入的所有字符内容 161 | lines = [view.substr(sublime.Region(view.line(l).a, l)) for l in locations] 162 | 163 | # 反转lines 164 | lines = [l[::-1] for l in lines] 165 | # print('lines', lines) 166 | 167 | # 正则匹配输入格式是否满足tag.attr,tag#attr的形式 168 | rex = re.compile("([\w-]+)([.#])(\w+)") 169 | expr = match(rex, lines[0]) 170 | # print('expr', expr) 171 | 172 | # 如果不匹配,则completion为空 173 | if not expr: 174 | return [] 175 | 176 | # 将语法格式的组成部分返回 177 | val, op, tag = rex.match(expr).groups() 178 | val = val[::-1] 179 | tag = tag[::-1] 180 | expr = expr[::-1] 181 | 182 | # 生成completion语法 183 | if op == '.': 184 | snippet = '<{0} class=\"{1}\">$0'.format(tag, val) 185 | else: 186 | snippet = '<{0} id=\"{1}\">$0'.format(tag, val) 187 | 188 | # 返回 189 | return [(expr, snippet)] 190 | 191 | def get_attribute_completions(self, view, locations, prefix, ch): 192 | 193 | # 以当前字符位置往前截取500个字符用于标签匹配查找 194 | SEARCH_LIMIT = 500 195 | pt = locations[0] 196 | search_start = max(0, pt - SEARCH_LIMIT - len(prefix)) 197 | line = view.substr(sublime.Region(search_start, pt + SEARCH_LIMIT)) 198 | 199 | # 表示当前位置之前的字符 200 | line_head = line[0:pt - search_start] 201 | 202 | # 标签当前位置之后的字符 203 | line_tail = line[pt - search_start:] 204 | 205 | # 已存在的属性列表 206 | exist_attr = [] 207 | 208 | # 当前键入的属性名称 209 | attr = '' 210 | 211 | # 找到距离当前输入位置最近的标签名 212 | i = len(line_head) - 1 213 | tag = None 214 | space_index = len(line_head) 215 | while i >= 0: 216 | c = line_head[i] 217 | if c == '<': 218 | tag = line_head[i + 1:space_index] 219 | break 220 | elif c == ' ' or c == '\t' or c == '\n': 221 | space_index = i 222 | i -= 1 223 | 224 | # 找到当前tag当前位置之前已经键入的attr列表 225 | i = len(line_head) - 1 226 | space_index = len(line_head) 227 | while i >= 0: 228 | c = line_head[i] 229 | if c == ' ' or c == '\t' or c == '\n': 230 | attr_value = line_head[i + 1:space_index] 231 | if attr_value != '': 232 | exist_attr.append(attr_value) 233 | space_index = i 234 | elif c == '<': 235 | break 236 | elif c == '=': 237 | space_index = i 238 | i -= 1 239 | 240 | # 找到当前tag当前位置之后已经键入的attr列表 241 | i = 0 242 | space_index = 0 243 | while i < len(line_tail): 244 | c = line_tail[i] 245 | if c == ' ' or c == '\t' or c == '\n': 246 | space_index = i 247 | elif c == '>' or c == '<': 248 | break 249 | elif c == '=': 250 | attr_value = line_tail[space_index+1:i] 251 | if attr_value != '': 252 | exist_attr.append(attr_value) 253 | space_index = i 254 | i += 1 255 | 256 | # 这里如果没有成员,取索引0会报错 257 | if len(exist_attr) > 0: 258 | attr = exist_attr[0] 259 | 260 | # 打印测试 261 | # print('tag', tag, '____end') 262 | # print('exist_attr', exist_attr) 263 | # print('attr', attr) 264 | 265 | # 检测标签的有效性 266 | if not tag: 267 | return [] 268 | 269 | # 如果标签没有结束,则自动加上>符号 270 | suffix = '>' 271 | for c in line_tail: 272 | if c == '>': 273 | # 找到了标签的结束标记 274 | suffix = '' 275 | break 276 | elif c == '<': 277 | # 发现了另一个打开的标签,则需要将当前标签结束 278 | break 279 | 280 | # 属性值的completion列表 281 | if ch == '"': 282 | attr_list = setting_cache.tag_data.get(tag, {}).get('attrs', {}) 283 | value_list = [] 284 | for item in attr_list: 285 | if item.get('name', '') == attr: 286 | value_list = item.get('enum', []) 287 | value_completions = [(item.get('value') + '\tValue', item.get('value')) for item in value_list] 288 | return value_completions 289 | 290 | # 属性名称的completion列表 291 | # 如果在字符串作用下则不输出 292 | elif not view.match_selector(locations[0], "text.wxml string.quoted"): 293 | attr_data = {} 294 | attr_list = [] 295 | attr_list.extend(setting_cache.tag_data.get(tag, {}).get('attrs', [])) 296 | 297 | # 按键值对记录数据 298 | for item in attr_list: 299 | attr_data.setdefault(item.get('name', ''), item) 300 | 301 | # 合并公共属性 302 | for item in setting_cache.global_attributes: 303 | # 直接是一个字典 304 | if str(type(item)).find('dict') >= 0: 305 | attr_list.append(item) 306 | attr_data.setdefault(item.get('name', ''), item) 307 | # 还可以是字符串 308 | else: 309 | attr_list.append({"name": item, "type": "string", "def": "", "desc": ""}) 310 | attr_data.setdefault(item, {"name": item, "type": "string", "def": "", "desc": ""}) 311 | 312 | attr_name_list = [(item.get('name', '')) for item in attr_list] 313 | attr_completions = [] 314 | 315 | # 如果为冒号,则需要对属性列表进行过滤 316 | temp_list = [] 317 | temp_obj = {} 318 | if ch == ':' or ch == '.' or ch == '@': 319 | for name in attr_name_list: 320 | match_result = name.find(attr) == 0 321 | if match_result: 322 | simple_attr = name.replace(attr, '') 323 | temp_obj.setdefault(simple_attr, name) 324 | temp_list.append(simple_attr) 325 | attr_name_list = temp_list 326 | 327 | # 将已存在属性移除 328 | for name in exist_attr: 329 | try: 330 | # 如果list中不存在值,将报错 331 | attr_name_list.remove(name) 332 | except ValueError: 333 | test = '' 334 | 335 | # 生成最终的属性列表 336 | for name in attr_name_list: 337 | # 由于可能通过冒号或者点号进行了过滤, 338 | # 所以需要得出原配置列表的属性名称, 339 | # 并得出属性的相关数据。 340 | attr_name = name 341 | if temp_obj.get(name, False): 342 | attr_name = temp_obj.get(name, '') 343 | attr_item = attr_data.get(attr_name, {}) 344 | 345 | #获取属性数据 346 | type_value = attr_item.get('type', 'string') 347 | desc_value = attr_item.get('desc', '') 348 | def_value = attr_item.get('def', '') 349 | 350 | comp_name = name + '\tAttr' 351 | comp_value = name + '="${1:' + def_value + '}"' 352 | 353 | # 不同属性值类型,输出不一样的补全方案 354 | if type_value == 'mustache': 355 | comp_value = name + '="{{$1}}"' 356 | elif type_value == 'prop': 357 | comp_value = name 358 | elif type_value == 'boolean': 359 | if def_value == 'false': 360 | comp_value = name 361 | else: 362 | comp_value = name + '="{{' + '${1:false}' + '}}"' 363 | 364 | if suffix == '>': 365 | comp_value += '$2' + suffix 366 | 367 | attr_completions.append((comp_name, comp_value)) 368 | 369 | return attr_completions 370 | 371 | # def matchByFuzzy(self, cont, fuzzy): 372 | # # 模糊匹配 373 | # reStr = '' 374 | # for char in fuzzy: 375 | # if char == '.': 376 | # reStr += '\.' + '.*' 377 | # else: 378 | # reStr += char + '.*' 379 | 380 | # rex = re.compile(reStr) 381 | # result = match(rex, cont) 382 | 383 | # if not result: 384 | # return False 385 | # else: 386 | # return True 387 | 388 | -------------------------------------------------------------------------------- /WXML_completions.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // 自定义标签的completion列表 3 | "custom_completion": [ 4 | ["block:if\tTag", "block wx:if=\"{{$1}}\">$0"], 5 | ["block:elif\tTag", "block wx:elif=\"{{$1}}\">$0"], 6 | ["block:else\tTag", "block wx:else>$0"], 7 | ["block:for\tTag", "block wx:for=\"{{$1}}\">$0"], 8 | ["block:for:for\tTag", "block wx:for=\"{{$1}}\" wx:for-index=\"${2:idx}\" wx:for-item=\"${3:item}\">$4"], 9 | ["block:for:key\tTag", "block wx:for=\"{{$1}}\" wx:for-index=\"${2:idx}\" wx:for-item=\"${3:item}\" wx:key=\"${4:*this}\">$5"], 10 | ["view:class\tTag", "view class=\"$1\">$0"], 11 | ["view:id\tTag", "view id=\"$1\">$0"], 12 | ["view:if\tTag", "view wx:if=\"{{$1}}\">$0"], 13 | ["view:elif\tTag", "view wx:elif=\"{{$1}}\">$0"], 14 | ["view:else\tTag", "view wx:else>$0"], 15 | ["view:for\tTag", "view wx:for=\"{{$1}}\">$0"], 16 | ["view:for:for\tTag", "view wx:for=\"{{$1}}\" wx:for-index=\"${2:idx}\" wx:for-item=\"${3:item}\">$4"], 17 | ["view:for:key\tTag", "view wx:for=\"{{$1}}\" wx:for-index=\"${2:idx}\" wx:for-item=\"${3:item}\" wx:key=\"${4:*this}\">$5"], 18 | ["text:class\tTag", "text class=\"$1\">$0"], 19 | ["image\tTag", "image class=\"$1\" src=\"$2\"${3: mode=\"${4:scaleToFill}\"}>"], 20 | ["navigator\tTag", "navigator class=\"$1\" url=\"$2\"${3: open-type=\"${4:navigate}\"}>$0"], 21 | ["include\tTag", "include src=\"${1}\"/>"], 22 | ["import\tTag", "import src=\"${1}\"/>"], 23 | ["template\tTag", "template name=\"${1}\">${0}"], 24 | ["template:is\tTag", "