├── Comments.tmPreferences ├── Default.sublime-commands ├── README.md ├── example.v ├── example_test.v ├── img ├── assert-failures.png ├── command-palette.png ├── inline-errors.png ├── install-package.png ├── install-v.png └── symbols.png ├── v.sublime-syntax ├── v.tmPreferences └── vlang-sublime.py /Comments.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | scope 5 | source.v 6 | settings 7 | 8 | shellVariables 9 | 10 | 11 | name 12 | TM_COMMENT_START 13 | value 14 | // 15 | 16 | 17 | name 18 | TM_COMMENT_START_2 19 | value 20 | /* 21 | 22 | 23 | name 24 | TM_COMMENT_END_2 25 | value 26 | */ 27 | 28 | 29 | name 30 | TM_COMMENT_DISABLE_INDENT_2 31 | value 32 | yes 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "V: Build", 4 | "command": "v", 5 | "args": { 6 | "cmd": "${file_path}", 7 | } 8 | }, 9 | { 10 | "caption": "V: Build File", 11 | "command": "v", 12 | "args": { 13 | "cmd": "${file}", 14 | } 15 | }, 16 | { 17 | "caption": "V: Format File", 18 | "command": "v", 19 | "args": { 20 | "cmd": "fmt -w ${file}", 21 | "output": false, 22 | } 23 | }, 24 | { 25 | "caption": "V: Format Module", 26 | "command": "v", 27 | "args": { 28 | "cmd": "fmt -w ${file_path}", 29 | "output": false, 30 | } 31 | }, 32 | { 33 | "caption": "V: Format Project", 34 | "command": "v", 35 | "args": { 36 | "cmd": "fmt -w ${folder}", 37 | "output": false, 38 | } 39 | }, 40 | { 41 | "caption": "V: Run File", 42 | "command": "v", 43 | "args": { 44 | "cmd": "run ${file}", 45 | } 46 | }, 47 | { 48 | "caption": "V: Run", 49 | "command": "v", 50 | "args": { 51 | "cmd": "run ${file_path}", 52 | } 53 | }, 54 | { 55 | "caption": "V: Test", 56 | "command": "v", 57 | "args": { 58 | "cmd": "test ${file_path}", 59 | } 60 | }, 61 | { 62 | "caption": "V: Test File", 63 | "command": "v", 64 | "args": { 65 | "cmd": "test ${file}", 66 | } 67 | }, 68 | { 69 | "caption": "V: Update", 70 | "command": "v", 71 | "args": { 72 | "cmd": "up", 73 | } 74 | }, 75 | ] 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sublime Text Support for the V Programming Language 2 | 3 | This bundle provides [V](https://vlang.io/) syntax highlighting for Sublime 4 | Text. 5 | 6 | ## Installation 7 | 8 | You can install the [V](https://packagecontrol.io/packages/V) package with 9 | [Package Control](https://packagecontrol.io): 10 | 11 | ![Package Control: Install Package](img/install-package.png) 12 | 13 | ![Package Control: Install Package](img/install-v.png) 14 | 15 | For code completion you can 16 | [install vlang/vls for Sublime Text 3 or 4](https://github.com/vlang/vls#sublime-text-3-and-4). 17 | 18 | ## Features 19 | 20 | Commands for building, running, testing, formatting and updating V itself: 21 | 22 | ![Command Palette](img/command-palette.png) 23 | 24 | Inline errors and warnings: 25 | 26 | ![Inline errors and warnings](img/inline-errors.png) 27 | 28 | Quickly goto to any symbol: 29 | 30 | ![Goto any symbol](img/symbols.png) 31 | 32 | Show assertion failures inline: 33 | 34 | ![Assert failures](img/assert-failures.png) 35 | 36 | ## Command Palette 37 | 38 | Any commands that generate output will show in a new window called "V". You may 39 | leave this tab open an any future runs will be appended. 40 | 41 | ### V: Build 42 | 43 | Build the current module (directory). 44 | 45 | ### V: Build File 46 | 47 | Build the current file. 48 | 49 | ### V: Format File 50 | 51 | Format the current file. 52 | 53 | ### V: Format Module 54 | 55 | Format the current module. 56 | 57 | ### V: Format Project 58 | 59 | Format the current project. 60 | 61 | ### V: Run 62 | 63 | Execute `v run` on the current module (directory). 64 | 65 | ### V: Run Test 66 | 67 | Execute `v run` only on the current file. 68 | 69 | ### V: Test 70 | 71 | Execute `v test` only on the current module (directory). 72 | 73 | ### V: Test File 74 | 75 | Execute `v test` only on the current file. 76 | 77 | ### V: Update 78 | 79 | Update V to the latest version (`v up`). 80 | 81 | ## Custom Commands 82 | 83 | ```json 84 | { 85 | "caption": "Build Prod", 86 | "command": "v", 87 | "args": { 88 | "cmd": "-prod ${module}", // required (string) 89 | "output": true, // optional (boolean) 90 | } 91 | }, 92 | ``` 93 | 94 | - `cmd` is required and in the example above will construct and execute 95 | `v -prod "some/folder/path"`. 96 | - `output` is optional (defaults to `true`). If `false`, the output is not shown 97 | in the "V" window. This is useful for commands you intend to be silent (such as 98 | `v fmt`). 99 | 100 | See 101 | [Sublime Variables](https://www.sublimetext.com/docs/build_systems.html#variables) 102 | for full list, but some common ones are: 103 | 104 | - `${file}` for the current file path. 105 | - `${file_path}` for the directory the current file exists in (the module). 106 | - `${folder}` for the project directory. 107 | 108 | ## Contributing 109 | 110 | Note: Make sure you uninstall the package if it's already installed with 111 | "Package Control > Remove Package..." 112 | 113 | Now clone the package locally: 114 | 115 | ```sh 116 | cd ~/Library/Application\ Support/Sublime\ Text/Packages 117 | git clone https://github.com/elliotchance/vlang-sublime.git 118 | ``` 119 | -------------------------------------------------------------------------------- /example.v: -------------------------------------------------------------------------------- 1 | // This file is included as a way to test commands with a V file. 2 | 3 | module main 4 | 5 | import os 6 | 7 | #flag -lsqlite3 8 | #include "sqlite3.h" 9 | 10 | [unsafe] 11 | struct Foo { 12 | bar int 13 | baz string 14 | } 15 | 16 | pub interface Writer { 17 | write() ?f64 18 | } 19 | 20 | fn (foo Foo) write() ?f64 { 21 | return -1.23 * 0xAF 22 | } 23 | 24 | enum Color { 25 | red 26 | green 27 | blue 28 | } 29 | 30 | fn main() { 31 | mut a := 4 + 5 32 | b := 6 33 | println(a) 34 | 35 | foo := Writer(Foo{}) 36 | if foo is Writer { 37 | println(foo) 38 | } 39 | 40 | $if windows { 41 | bar := Foo{} 42 | println(bar as Foo) 43 | println(@LINE) 44 | } 45 | 46 | mut color := Color.red 47 | color = .green 48 | println(color) // "green" 49 | match color { 50 | .red { println('the color was red') } 51 | .green { println('the color was green') } 52 | .blue { println('the color was blue') } 53 | } 54 | } 55 | 56 | fn add(a int, b int) int { 57 | return a + b 58 | } 59 | -------------------------------------------------------------------------------- /example_test.v: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | fn test_something() { 4 | assert add(3, 5) == 7 5 | } 6 | 7 | fn test_interpolation() { 8 | one, two := 1, 2 9 | assert '1 + 2 = ${1 + 2}' == "1 + 2 = 4" 10 | assert "$one + $two = 3" == "1 + 2 = 3" 11 | } 12 | 13 | fn test_hi() { 14 | assert 1 == 1 15 | } 16 | -------------------------------------------------------------------------------- /img/assert-failures.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotchance/vlang-sublime/6e457ca6e2506407c1cbcdcf2ac110b706b991c0/img/assert-failures.png -------------------------------------------------------------------------------- /img/command-palette.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotchance/vlang-sublime/6e457ca6e2506407c1cbcdcf2ac110b706b991c0/img/command-palette.png -------------------------------------------------------------------------------- /img/inline-errors.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotchance/vlang-sublime/6e457ca6e2506407c1cbcdcf2ac110b706b991c0/img/inline-errors.png -------------------------------------------------------------------------------- /img/install-package.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotchance/vlang-sublime/6e457ca6e2506407c1cbcdcf2ac110b706b991c0/img/install-package.png -------------------------------------------------------------------------------- /img/install-v.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotchance/vlang-sublime/6e457ca6e2506407c1cbcdcf2ac110b706b991c0/img/install-v.png -------------------------------------------------------------------------------- /img/symbols.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elliotchance/vlang-sublime/6e457ca6e2506407c1cbcdcf2ac110b706b991c0/img/symbols.png -------------------------------------------------------------------------------- /v.sublime-syntax: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | name: V 4 | file_extensions: 5 | - v 6 | scope: source.v 7 | 8 | variables: 9 | ident: \b[A-Za-z_][A-Za-z_0-9]*\b 10 | 11 | contexts: 12 | main: 13 | # Most simple entities. 14 | - include: enum-value 15 | - include: comment-single-line 16 | - include: comment-multi-line 17 | - include: string-double 18 | - include: string-single 19 | - include: string-character 20 | - include: numbers 21 | - include: operator 22 | - include: attributes 23 | - include: punctuation 24 | 25 | # More complex constructs. 26 | - include: as-is 27 | - include: variable-definition 28 | - include: method 29 | - include: fn 30 | - include: interface 31 | - include: struct 32 | - include: type 33 | - include: compile-time 34 | - include: enum 35 | - include: flag 36 | - include: import 37 | - include: include 38 | 39 | # keywords needs to be a last resort as some of the specific keywords above 40 | # should try to match first. 41 | - include: keyword 42 | 43 | punctuation: 44 | - match: \. 45 | scope: punctuation.delimiter.period.dot.v 46 | - match: \, 47 | scope: punctuation.delimiter.comma.v 48 | - match: ':' 49 | scope: punctuation.separator.key-value.colon.v 50 | - match: \; 51 | scope: punctuation.definition.other.semicolon.v 52 | - match: \? 53 | scope: punctuation.definition.other.questionmark.v 54 | 55 | flag: 56 | - match: ^\s*(#flag)\s+(.*?)$ 57 | captures: 58 | 0: meta.flag.v 59 | 1: keyword.flag.v 60 | 2: string.quoted.single.v 61 | 62 | include: 63 | - match: ^\s*(#include)\s+(.*?)$ 64 | captures: 65 | 0: meta.include.v 66 | 1: keyword.include.v 67 | 2: string.quoted.double.v 68 | 69 | import: 70 | - match: ^\s*(import)\s+(.*?)$ 71 | captures: 72 | 0: meta.import.v 73 | 1: keyword 74 | 2: entity.name.import.v 75 | 76 | compile-time: 77 | - match: (\$if|\$else|$for|\$embed_file|\$tmpl|\$env) 78 | captures: 79 | 1: keyword.control.v 80 | - match: (@FN|@METHOD|@MOD|@STRUCT|@FILE|@LINE|@COLUMN|@VEXE|@VEXEROOT|@VHASH|@VMOD_FILE|@VMODROOT) 81 | captures: 82 | 1: variable.language.v 83 | 84 | attributes: 85 | - match: ^\s*(\[(deprecated|unsafe_fn|typedef|live|inline|flag)\]) 86 | captures: 87 | 1: meta.definition.attribute.v 88 | 89 | as-is: 90 | - match: \b(as|is)\s*({{ident}})\b 91 | captures: 92 | 1: keyword 93 | 2: meta.type.v 94 | 95 | numbers: 96 | - match: (?:(?:[-]?)(?:[0-9e]*)(?:[.]){1}(?:[0-9]+)) 97 | scope: constant.numeric.float.decimal.v 98 | - match: \b(?:0[xX])(?:[0-9a-fA-F]+) 99 | scope: constant.numeric.hex.v 100 | - match: \b(?:[-]?)(?:[0-9]+) 101 | scope: constant.numeric.integer.decimal.v 102 | 103 | comment-single-line: 104 | - match: // 105 | scope: punctuation.definition.comment.v 106 | push: 107 | - meta_scope: comment.line.v 108 | - match: $\n? 109 | pop: true 110 | 111 | comment-multi-line: 112 | - match: /\* 113 | scope: punctuation.definition.comment.begin.v 114 | push: 115 | - meta_scope: comment.block.v 116 | - match: \*/ 117 | scope: punctuation.definition.comment.end.v 118 | pop: true 119 | - match: ^\s*(\*)(?!/) 120 | captures: 121 | 1: punctuation.definition.comment.v 122 | 123 | variable-definition: 124 | - match: '(mut)\s+({{ident}})\s*(:=)' 125 | captures: 126 | 1: keyword 127 | 2: variable.other.readwrite 128 | 3: operator 129 | - match: '({{ident}})\s*(:=)' 130 | captures: 131 | 1: variable.other.constant 132 | 2: operator 133 | 134 | struct: 135 | - match: \bstruct\b 136 | captures: 137 | 0: keyword 138 | push: 139 | - match: '{{ident}}' 140 | # TODO(elliotchance): I don't know why the more appropriate 141 | # \"entity.name.struct.v\" does not work here? Tested with ST4. 142 | scope: entity.name.type.struct.v 143 | pop: true 144 | 145 | interface: 146 | - match: \binterface\b 147 | captures: 148 | 0: keyword 149 | push: 150 | - match: '{{ident}}' 151 | # TODO(elliotchance): I don't know why the more appropriate 152 | # \"entity.name.interface.v\" does not work here? Tested with ST4. 153 | scope: entity.name.type.interface.v 154 | pop: true 155 | 156 | enum: 157 | - match: \benum\b 158 | captures: 159 | 0: keyword 160 | push: 161 | - match: '{{ident}}' 162 | scope: entity.name.enum.v 163 | pop: true 164 | 165 | enum-value: 166 | - match: (\.{{ident}}) 167 | captures: 168 | 1: entity.name.constant.v 169 | 170 | type: 171 | - match: \b(type)\b\s*({{ident}}) 172 | captures: 173 | 1: keyword 174 | 2: entity.name.type.v 175 | 176 | # We list all keywords even through some other rules may have specific 177 | # handling if there is a syntax error we always want to highlight the keywords 178 | # themselves. 179 | # 180 | # https://github.com/vlang/v/blob/master/doc/docs.md#appendix-i-keywords 181 | # https://github.com/vlang/v/blob/master/doc/docs.md#v-types 182 | keyword: 183 | - match: \b(any|bool|byte|f32|f64|i128|i16|i64|i8|int|isize|rune|string|u128|u16|u32|u64|usize|voidptr|map)\b 184 | scope: storage.type.v 185 | - match: \b(true|false|none)\b 186 | scope: constant.language 187 | - match: \b(for|goto|go|lock|return|rlock|assert|break|continue|defer)\b 188 | scope: keyword.control.v 189 | - match: \b(if|else|or|select|match)\b 190 | scope: keyword.control.conditional.v 191 | - match: \bimport\b 192 | scope: keyword.control.import.v 193 | - match: \b(in|as|is|module)\b 194 | scope: keyword.operator.word.v 195 | - match: \bstruct\b 196 | scope: keyword.declaration.struct.v 197 | - match: \binterface\b 198 | scope: keyword.declaration.interface.v 199 | - match: \bfn\b 200 | scope: keyword.declaration.function.v 201 | - match: \b(mut|pub|shared|const|unsafe|volatile|static|atomic)\b 202 | scope: storage.modifier.v 203 | - match: \benum\b 204 | scope: keyword.declaration.enum.v 205 | - match: \btype\b 206 | scope: keyword.declaration.type.v 207 | - match: \bunion\b 208 | scope: keyword.declaration.union.v 209 | - match: \b(asm|embed|sizeof|typeof|__offsetof)\b 210 | scope: keyword.other.v 211 | 212 | operator: 213 | - match: '([+*/%-:&|^]|<<|>>>?)=' 214 | scope: keyword.operator.assignment 215 | - match: '[+*/%-]' 216 | scope: keyword.operator.arithmetic 217 | - match: '!|&&|\|\|' 218 | scope: keyword.operator.logical 219 | - match: '[~&|^]|<<|>>>?' 220 | scope: keyword.operator.bitwise 221 | 222 | method: 223 | - match: (fn)\s*\((mut\s+)?{{ident}}\s+(({{ident}})\)\s*({{ident}})) 224 | captures: 225 | 1: keyword 226 | 2: keyword 227 | 3: meta.indexed-unit.v 228 | 4: meta.type.v 229 | 5: entity.name.function.v 230 | 231 | fn: 232 | - match: \bfn\b 233 | captures: 234 | 0: keyword 235 | push: 236 | # Ignore inline functions (unnamed). 237 | - match: \( 238 | pop: true 239 | 240 | - match: '{{ident}}' 241 | scope: entity.name.function.v 242 | pop: true 243 | 244 | string-interpolation: 245 | - match: '\$\{' 246 | scope: punctuation.definition.string.interpolated.element.begin.v 247 | push: 248 | - meta_scope: source.v.embedded 249 | - match: '\}' 250 | scope: punctuation.definition.string.interpolated.element.end.v 251 | pop: true 252 | - include: 'main' 253 | - match: '\$[A-Za-z_0-9.()]*' 254 | scope: source.v.embedded 255 | - match: \\([0-7]{3}|[\$abfnrtv\\'"]|x[0-9a-fA-F]{2}|u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}) 256 | scope: constant.character.escape.v 257 | - match: \\[^0-7\$xuUabfnrtv\'"] 258 | scope: invalid.illegal.unknown-escape.v 259 | 260 | string-single: 261 | - match: "'" 262 | push: 263 | - meta_scope: string.interpolated.v 264 | - include: string-interpolation 265 | - match: "'" 266 | pop: true 267 | 268 | string-double: 269 | - match: '"' 270 | push: 271 | - meta_scope: string.interpolated.v 272 | - include: string-interpolation 273 | - match: '"' 274 | pop: true 275 | 276 | string-character: 277 | - match: '`' 278 | push: 279 | - meta_scope: string.interpolated.v 280 | - include: string-interpolation 281 | - match: '`' 282 | pop: true 283 | -------------------------------------------------------------------------------- /v.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | scope 6 | source.v meta.indexed-unit.v 7 | settings 8 | 9 | showInIndexedSymbolList 10 | 1 11 | symbolIndexTransformation 12 | 13 | s/\)\s*/\./; 14 | 15 | showInSymbolList 16 | 1 17 | symbolTransformation 18 | 19 | s/mut\s+|\)\s*/\./; 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /vlang-sublime.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | import os 4 | import threading 5 | import subprocess 6 | import re 7 | 8 | run_output = None 9 | test_failures = {} 10 | assert_failures = {} 11 | 12 | def setup_output(window): 13 | global run_output 14 | 15 | # To prevent a new tab every time, let's reuse the output window. The 16 | # buffer_id will be zero if it was closed. 17 | if run_output is None or run_output.buffer_id() == 0: 18 | run_output = window.new_file() 19 | run_output.set_scratch(True) 20 | run_output.set_name('V') 21 | 22 | window.focus_view(run_output) 23 | 24 | return run_output 25 | 26 | def get_test_name(line): 27 | matches = re.search(r"fn\s+(test_[a-zA-Z_0-9]+)", line) 28 | if matches is None: 29 | return None 30 | 31 | return matches.group(1) 32 | 33 | class EventListener(sublime_plugin.EventListener): 34 | def on_activated(self, view): 35 | self.refresh(view) 36 | self.on_modified(view) 37 | 38 | def on_post_save(self, view): 39 | self.refresh(view) 40 | 41 | def on_modified(self, view): 42 | file_path = view.file_name() 43 | if file_path is None: 44 | return 45 | 46 | global test_failures, assert_failures 47 | 48 | # If this is a test file, extract all the test names and refresh their 49 | # status. 50 | all_lines = sublime.Region(0, view.size()) 51 | line_regions = view.split_by_newlines(all_lines) 52 | test_failure_regions = [] 53 | assert_failure_regions = [] 54 | line_number = 1 55 | view.erase_phantoms("assert_phantoms") 56 | for line_region in line_regions: 57 | line_content = view.substr(line_region) 58 | test_name = get_test_name(line_content) 59 | if test_name is not None: 60 | key = file_path + ':' + test_name 61 | 62 | if key in test_failures: 63 | test_failure_regions.append(line_region) 64 | 65 | key = file_path + ':' + str(line_number) 66 | if key in assert_failures: 67 | # Skip whitespace at the start of the line so its neater. 68 | prefix_whitespace = len(line_content) - len(line_content.lstrip()) 69 | assert_failure_regions.append(sublime.Region(line_region.a + prefix_whitespace, line_region.b)) 70 | 71 | raw = assert_failures[key] 72 | html = '
' + raw.replace('\n', '
').replace(' ', ' ') + '
' 73 | view.add_phantom("assert_phantoms", line_region, html, sublime.LAYOUT_BLOCK) 74 | 75 | line_number += 1 76 | 77 | view.add_regions("v_test_failures", test_failure_regions, "region.redish", "circle", 78 | sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE) 79 | 80 | view.add_regions("v_assert_failures", assert_failure_regions, "region.redish", "", 81 | sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE | sublime.DRAW_SQUIGGLY_UNDERLINE) 82 | 83 | def refresh(self, view): 84 | # Only applies to v files. 85 | file_path = view.file_name() 86 | if file_path is None or not file_path.endswith('.v'): 87 | return 88 | 89 | # We need to test the whole module, otherwise it might not know about 90 | # entities in other files of the same module. 91 | module = os.path.dirname(file_path) 92 | 93 | # "-shared" prevents it from complaining if this is not a main module. 94 | command = "v -check -nocolor -shared -message-limit -1 \"" + module + "\"" 95 | proc = subprocess.Popen( 96 | [os.environ['SHELL'], '-c', command], 97 | shell=False, 98 | stdout=subprocess.PIPE, 99 | stderr=subprocess.STDOUT, 100 | universal_newlines=True, 101 | env=os.environ.copy() 102 | ) 103 | 104 | full_output = '' 105 | while True: 106 | out = proc.stdout.read(1) 107 | if out == '' and proc.poll() != None: 108 | break 109 | 110 | full_output += out 111 | 112 | error_regions = [] 113 | error_annotations = [] 114 | warning_regions = [] 115 | warning_annotations = [] 116 | for line in full_output.split('\n'): 117 | matches = re.search(r"(\d+):(\d+): (.*)", line) 118 | 119 | # TODO(elliotchance): We are only showing the errors/warnings for 120 | # this view, but we should also apply any output to other open 121 | # files to prevent needing to rerun the checker whenever a tab 122 | # changes. 123 | if matches is None or file_path not in line: 124 | continue 125 | 126 | offset = view.text_point(int(matches.group(1))-1, 0) + int(matches.group(2)) - 1 127 | 128 | # The error message may give us a token which we can use to 129 | # determine the length. We *could* use the "~~~" that appears later 130 | # in the stream, but thats a bit fidely, so I'm just going to use 131 | # the token or goto the end of the line. 132 | symbol = re.search(r"`(.*?)`", line) 133 | if symbol is None: 134 | end = view.text_point(int(matches.group(1)), 0)-1 135 | else: 136 | end = offset + len(symbol.group(1)) 137 | 138 | region = sublime.Region(offset, end) 139 | 140 | msg = ':'.join(line.split(':')[4:]) 141 | if 'warning:' in line: 142 | warning_regions.append(region) 143 | warning_annotations.append(msg) 144 | else: 145 | error_regions.append(region) 146 | error_annotations.append(msg) 147 | 148 | view.add_regions("v_errors", error_regions, "region.redish", "dot", 149 | sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE | sublime.DRAW_SQUIGGLY_UNDERLINE, 150 | error_annotations) 151 | 152 | view.add_regions("v_warnings", warning_regions, "region.yellowish", "dot", 153 | sublime.DRAW_NO_FILL | sublime.DRAW_NO_OUTLINE | sublime.DRAW_SQUIGGLY_UNDERLINE, 154 | warning_annotations, 'yellow') 155 | 156 | class VCommand(sublime_plugin.WindowCommand): 157 | def run(self, cmd, output=True): 158 | global test_failures, assert_failures 159 | 160 | # Make sure we get the file path before we open a new tab. 161 | cur_view = self.window.active_view() 162 | file = cur_view.file_name() 163 | module = os.path.dirname(cur_view.file_name()) 164 | 165 | # Substitute variables. It would be nice to use 166 | # sublime.expand_variables() here instead, but that does not escape 167 | # spaces in paths. 168 | variables = sublime.active_window().extract_variables() 169 | 170 | # We have some extra variables we can use. 171 | sel = cur_view.sel() 172 | if len(sel) == 1: 173 | line = cur_view.substr(cur_view.full_line(sel[0])) 174 | matches = re.search(r"fn\s+(test_[a-zA-Z_0-9]+)", line) 175 | if matches is not None: 176 | variables['function_at_cursor'] = matches.group(1) 177 | 178 | look_for_failed_tests = False 179 | if cmd.startswith("test"): 180 | look_for_failed_tests = True 181 | 182 | # Clear any existing tests within this file. 183 | test_failures = {k: v[k] for k in test_failures if not k.startswith(file + ":")} 184 | assert_failures = {k: v[k] for k in assert_failures if not k.startswith(file + ":")} 185 | 186 | cmd = "v " + cmd 187 | for variable in variables: 188 | cmd = cmd.replace('${' + variable + '}', '"' + variables[variable] + '"') 189 | 190 | view = None 191 | if output: 192 | view = setup_output(self.window) 193 | view.run_command('insert_view', { 'string': '$ ' + cmd + '\n' }) 194 | 195 | runner = Runner(cmd, os.environ['SHELL'], os.environ.copy(), view, output, look_for_failed_tests) 196 | runner.start() 197 | 198 | class InsertViewCommand(sublime_plugin.TextCommand): 199 | def run(self, edit, string=''): 200 | self.view.set_read_only(False) 201 | self.view.insert(edit, self.view.size(), string) 202 | self.view.set_read_only(True) 203 | 204 | class Runner(threading.Thread): 205 | def __init__(self, command, shell, env, view, output, look_for_failed_tests): 206 | self.stdout = None 207 | self.stderr = None 208 | self.command = command 209 | self.shell = shell 210 | self.env = env 211 | self.view = view 212 | self.output = output 213 | self.look_for_failed_tests = look_for_failed_tests 214 | threading.Thread.__init__(self) 215 | 216 | def run(self): 217 | global test_failures, assert_failures 218 | 219 | proc = subprocess.Popen( 220 | [self.shell, '-c', self.command], 221 | shell=False, 222 | stdout=subprocess.PIPE, 223 | stderr=subprocess.STDOUT, 224 | universal_newlines=True, 225 | env=self.env 226 | ) 227 | 228 | full_output = '' 229 | while True: 230 | out = proc.stdout.read(1) 231 | if out == '' and proc.poll() != None: 232 | break 233 | if out != '': 234 | full_output += out 235 | 236 | if self.look_for_failed_tests: 237 | matches = re.findall(r"(.*_test\.v):(\d+): fn (test_.*)([\s\S]*?(?=\n{2,}))", full_output) 238 | for match in matches: 239 | test_failures[match[0] + ":" + match[2]] = True 240 | assert_failures[match[0] + ":" + match[1]] = match[3][1:] # Trim initial new line 241 | 242 | if self.output: 243 | self.view.run_command('insert_view', { 'string': full_output + '\n' }) 244 | --------------------------------------------------------------------------------