├── Cargo.sublime-syntax ├── Context.sublime-menu ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── Default.sublime-keymap ├── LICENSE.txt ├── Main.sublime-menu ├── RustComment.JSON-tmPreferences ├── RustComment.tmPreferences ├── RustEnhanced.sublime-build ├── RustEnhanced.sublime-commands ├── RustEnhanced.sublime-settings ├── RustEnhanced.sublime-syntax ├── RustIndent.JSON-tmPreferences ├── RustIndent.tmPreferences ├── RustSymbols.JSON-tmPreferences ├── RustSymbols.tmPreferences ├── SyntaxCheckPlugin.py ├── cargo_build.py ├── changelog └── 2.11.0.md ├── ci └── install-rust.sh ├── dependencies.json ├── images └── gutter │ ├── circle-error.png │ ├── circle-error@2x.png │ ├── circle-help.png │ ├── circle-help@2x.png │ ├── circle-none.png │ ├── circle-none@2x.png │ ├── circle-note.png │ ├── circle-note@2x.png │ ├── circle-warning.png │ ├── circle-warning@2x.png │ ├── shape-error.png │ ├── shape-error@2x.png │ ├── shape-help.png │ ├── shape-help@2x.png │ ├── shape-none.png │ ├── shape-none@2x.png │ ├── shape-note.png │ ├── shape-note@2x.png │ ├── shape-warning.png │ └── shape-warning@2x.png ├── messages.json ├── rust ├── __init__.py ├── batch.py ├── cargo_config.py ├── cargo_settings.py ├── levels.py ├── log.py ├── messages.py ├── opanel.py ├── rust_proc.py ├── rust_thread.py ├── semver.py ├── target_detect.py ├── themes.py └── util.py ├── snippets ├── Err.sublime-snippet ├── Ok.sublime-snippet ├── Some.sublime-snippet ├── allow.sublime-snippet ├── assert.sublime-snippet ├── assert_eq.sublime-snippet ├── attribute.sublime-snippet ├── bench.sublime-snippet ├── break.sublime-snippet ├── const.sublime-snippet ├── continue.sublime-snippet ├── dbg.sublime-snippet ├── debug.sublime-snippet ├── deny.sublime-snippet ├── derive.sublime-snippet ├── else.sublime-snippet ├── enum.sublime-snippet ├── eprintln.sublime-snippet ├── error.sublime-snippet ├── extern-crate.sublime-snippet ├── extern-fn.sublime-snippet ├── extern-mod.sublime-snippet ├── feature.sublime-snippet ├── fn.sublime-snippet ├── for.sublime-snippet ├── format.sublime-snippet ├── if-let.sublime-snippet ├── if.sublime-snippet ├── impl-trait.sublime-snippet ├── impl.sublime-snippet ├── info.sublime-snippet ├── let-mut.sublime-snippet ├── let.sublime-snippet ├── loop.sublime-snippet ├── macro_export.sublime-snippet ├── macro_rules.sublime-snippet ├── macro_use.sublime-snippet ├── main.sublime-snippet ├── match.sublime-snippet ├── mod.sublime-snippet ├── panic.sublime-snippet ├── plugin.sublime-snippet ├── println.sublime-snippet ├── repr-c.sublime-snippet ├── static.sublime-snippet ├── struct-tuple.sublime-snippet ├── struct-unit.sublime-snippet ├── struct.sublime-snippet ├── test.sublime-snippet ├── tests-mod.sublime-snippet ├── trace.sublime-snippet ├── trait.sublime-snippet ├── type.sublime-snippet ├── unimplemented.sublime-snippet ├── unreachable.sublime-snippet ├── use.sublime-snippet ├── warn.sublime-snippet ├── while-let.sublime-snippet └── while.sublime-snippet └── toggle_setting.py /Cargo.sublime-syntax: -------------------------------------------------------------------------------- 1 | %YAML 1.2 2 | --- 3 | # http://www.sublimetext.com/docs/3/syntax.html 4 | name: Cargo Build Results 5 | file_extensions: 6 | - rs 7 | scope: source.build_results 8 | 9 | variables: 10 | result_prefix: '[1-9]\d* ' 11 | test_prefix: '^test .* \.{3} ' 12 | 13 | contexts: 14 | main: 15 | # Loosely organized by order of appearance 16 | 17 | # comments the command run (as the previous syntax did) 18 | - match: '^\[Running: cargo.*\]$' 19 | scope: comment meta.command.cargo 20 | 21 | # matches the build logs 22 | - match: '^ {4}Blocking ' 23 | scope: markup.deleted.diff meta.blocking.cargo 24 | - match: '^ {4}Updating ' 25 | scope: markup.inserted.diff meta.updating.cargo 26 | - match: '^ {1}Downloading ' 27 | scope: markup.inserted.diff meta.downloading.cargo 28 | - match: '^ {1}Documenting ' 29 | scope: markup.inserted.diff meta.documenting.cargo 30 | - match: '^ {3}Compiling ' 31 | scope: markup.inserted.diff meta.compiling.cargo 32 | - match: '^ {4}Finished ' 33 | scope: markup.inserted.diff meta.finished.cargo 34 | - match: '^ {5}Running ' 35 | scope: markup.inserted.diff meta.running.cargo 36 | - match: '^ {3}Doc-tests' 37 | scope: markup.inserted.diff meta.doctests.cargo 38 | 39 | # matches the build result 40 | - match: '^(..[^:\n]*):(\d+):?(\d+)?:? ' 41 | scope: entity.name.filename meta.filename.cargo 42 | - match: '^(note|warning): ' 43 | scope: variable.parameter meta.warning.cargo 44 | - match: '^error: ' 45 | scope: message.error meta.error.cargo 46 | 47 | # matches the test results: 'test ... ' 48 | - match: '{{test_prefix}}(ok)\b' 49 | captures: 50 | 1: markup.inserted.diff meta.test_ok.cargo 51 | - match: '{{test_prefix}}(ignored)\b' 52 | captures: 53 | 1: markup.changed.diff meta.test_ignored.cargo 54 | - match: '{{test_prefix}}(FAILED)\b' 55 | captures: 56 | 1: markup.deleted.diff meta.test_failed.cargo 57 | # benches are aligned, so they may have extra spaces before the '...' 58 | - match: '^test .* +\.{3} (bench): +\d+' 59 | captures: 60 | 1: markup.deleted.diff meta.bench.cargo 61 | 62 | - match: '\bhelp:[\s\S]+$' 63 | scope: markup.inserted.diff meta.help.cargo 64 | # I don't know what this is 65 | - match: '^\s{4}[\S\s]+,\s([\w,\s-]+\.[A-Za-z]{2}):(\d+)' 66 | scope: message.error 67 | 68 | - match: '^failures:\n' 69 | scope: message.error meta.failures.cargo 70 | 71 | # matches the line 'test result: FAILED. 1 passed; 1 failed; 1 ignored; 0 measured; 0 filtered out' 72 | - match: '^(test result:) (?:(ok)|(FAILED))\.' 73 | captures: 74 | 1: variable.parameter meta.test_result.cargo 75 | 2: markup.inserted.diff meta.ok_result.cargo 76 | 3: invalid meta.fail_result.cargo 77 | push: test-result-counts 78 | 79 | # comments the logs sublime adds 80 | - match: '^\[Finished in \d+\.\d+s( with exit code .+)?\]$' 81 | set: 82 | - meta_scope: comment meta.sublime.cargo 83 | 84 | test-result-counts: 85 | - match: '{{result_prefix}}passed(?=;)' 86 | scope: markup.inserted.diff meta.passed_count.cargo 87 | - match: '{{result_prefix}}failed(?=;)' 88 | scope: markup.deleted.diff meta.fail_count.cargo 89 | - match: '{{result_prefix}}ignored(?=;)' 90 | scope: markup.changed.diff meta.ignore_count.cargo 91 | - match: '{{result_prefix}}measured(?=;)' 92 | scope: support.constant meta.measured_count.cargo 93 | - match: '\n' 94 | pop: true 95 | -------------------------------------------------------------------------------- /Context.sublime-menu: -------------------------------------------------------------------------------- 1 | // XXX 2 | // run/test/bench with args? 3 | // 4 | [ 5 | { 6 | "caption": "Rust", 7 | "id": "rust_context", 8 | "children": [ 9 | { 10 | "caption": "Clean", 11 | "command": "cargo_exec", 12 | "args": { 13 | "command": "clean" 14 | } 15 | }, 16 | { 17 | "caption": "Check", 18 | "command": "cargo_exec", 19 | "args": { 20 | "command": "check" 21 | } 22 | }, 23 | { 24 | "caption": "Clippy", 25 | "command": "cargo_exec", 26 | "args": { 27 | "command": "clippy" 28 | } 29 | }, 30 | { 31 | "caption": "-", 32 | }, 33 | { 34 | "caption": "Test Here", 35 | "command": "cargo_test_here", 36 | }, 37 | { 38 | "caption": "Test Current File", 39 | "command": "cargo_test_current_file", 40 | }, 41 | { 42 | "caption": "Test All", 43 | "command": "cargo_exec", 44 | "args": { 45 | "command": "test" 46 | } 47 | }, 48 | { 49 | "caption": "-", 50 | }, 51 | { 52 | "caption": "Bench Here", 53 | "command": "cargo_bench_here", 54 | }, 55 | { 56 | "caption": "Bench Current File", 57 | "command": "cargo_bench_current_file", 58 | }, 59 | { 60 | "caption": "Bench All", 61 | "command": "cargo_exec", 62 | "args": { 63 | "command": "bench" 64 | } 65 | }, 66 | { 67 | "caption": "-", 68 | }, 69 | { 70 | "caption": "Run This File", 71 | "command": "cargo_run_current_file", 72 | }, 73 | { 74 | "caption": "Doc", 75 | "command": "cargo_exec", 76 | "args": { 77 | "command": "doc", 78 | } 79 | }, 80 | { 81 | "caption": "Cancel Build", 82 | "command": "rust_cancel" 83 | }, 84 | { 85 | "caption": "Open Debug Log", 86 | "command": "rust_open_log" 87 | }, 88 | { 89 | "caption": "-", 90 | }, 91 | { 92 | "caption": "Clear Messages", 93 | "command": "rust_dismiss_messages", 94 | }, 95 | { 96 | "caption": "List All Messages", 97 | "command": "rust_list_messages", 98 | }, 99 | { 100 | "caption": "-", 101 | }, 102 | { 103 | "caption": "Open Settings", 104 | "command": "edit_settings", 105 | "args": 106 | { 107 | "base_file": "${packages}/Rust Enhanced/RustEnhanced.sublime-settings", 108 | "default": "{\n\t$0\n}\n" 109 | } 110 | }, 111 | { 112 | "caption": "Configure Cargo Build", 113 | "command": "cargo_configure", 114 | }, 115 | { 116 | "caption": "On-Save Checking", 117 | "command": "toggle_rust_syntax_setting", 118 | }, 119 | ] 120 | } 121 | ] 122 | -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | {"keys": ["f4"], "command": "rust_next_message", "context": 3 | [ 4 | {"key": "selector", "operator":"equal", "operand": "source.rust"} 5 | ] 6 | }, 7 | {"keys": ["shift+f4"], "command": "rust_prev_message", "context": 8 | [ 9 | {"key": "selector", "operator":"equal", "operand": "source.rust"} 10 | ] 11 | }, 12 | {"keys": ["ctrl+break"], "command": "rust_cancel", "context": 13 | [ 14 | {"key": "selector", "operator":"equal", "operand": "source.rust"} 15 | ] 16 | }, 17 | {"keys": ["escape"], "command": "rust_dismiss_messages", "context": 18 | [ 19 | {"key": "selector", "operator":"equal", "operand": "source.rust"}, 20 | {"key": "rust_has_messages", "operator": "equal", "operand": true} 21 | ] 22 | }, 23 | ] 24 | -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | {"keys": ["f4"], "command": "rust_next_message", "context": 3 | [ 4 | {"key": "selector", "operator":"equal", "operand": "source.rust"} 5 | ] 6 | }, 7 | {"keys": ["shift+f4"], "command": "rust_prev_message", "context": 8 | [ 9 | {"key": "selector", "operator":"equal", "operand": "source.rust"} 10 | ] 11 | }, 12 | {"keys": ["ctrl+c"], "command": "rust_cancel", "context": 13 | [ 14 | {"key": "selector", "operator":"equal", "operand": "source.rust"} 15 | ] 16 | }, 17 | {"keys": ["escape"], "command": "rust_dismiss_messages", "context": 18 | [ 19 | {"key": "selector", "operator":"equal", "operand": "source.rust"}, 20 | {"key": "rust_has_messages", "operator": "equal", "operand": true} 21 | ] 22 | }, 23 | ] 24 | -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | {"keys": ["f4"], "command": "rust_next_message", "context": 3 | [ 4 | {"key": "selector", "operator":"equal", "operand": "source.rust"} 5 | ] 6 | }, 7 | {"keys": ["shift+f4"], "command": "rust_prev_message", "context": 8 | [ 9 | {"key": "selector", "operator":"equal", "operand": "source.rust"} 10 | ] 11 | }, 12 | {"keys": ["ctrl+break"], "command": "rust_cancel", "context": 13 | [ 14 | {"key": "selector", "operator":"equal", "operand": "source.rust"} 15 | ] 16 | }, 17 | {"keys": ["escape"], "command": "rust_dismiss_messages", "context": 18 | [ 19 | {"key": "selector", "operator":"equal", "operand": "source.rust"}, 20 | {"key": "rust_has_messages", "operator": "equal", "operand": true} 21 | ] 22 | }, 23 | ] 24 | -------------------------------------------------------------------------------- /Default.sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | // Disable auto-pair for single quote since it is often used for lifetimes. 3 | { "keys": ["'"], "command": "insert_snippet", "args": {"contents": "'"}, "context": 4 | [{ "key": "selector", "operator": "equal", "operand": "source.rust" }] 5 | }, 6 | // ' in b'c' will skip past the end quote. 7 | { "keys": ["'"], "command": "move", "args": {"by": "characters", "forward": true}, "context": 8 | [ 9 | { "key": "setting.auto_match_enabled", "operator": "equal", "operand": true }, 10 | { "key": "selection_empty", "operator": "equal", "operand": true, "match_all": true }, 11 | { "key": "following_text", "operator": "regex_contains", "operand": "^'", "match_all": true }, 12 | { "key": "selector", "operator": "equal", "operand": "source.rust - punctuation.definition.string.begin" }, 13 | { "key": "eol_selector", "operator": "not_equal", "operand": "string.quoted.single - punctuation.definition.string.end", "match_all": true }, 14 | ] 15 | }, 16 | // b' will expand to b'' 17 | { "keys": ["'"], "command": "insert_snippet", "args": {"contents": "'$0'"}, "context": 18 | [ 19 | { "key": "setting.auto_match_enabled", "operator": "equal", "operand": true }, 20 | { "key": "selection_empty", "operator": "equal", "operand": true, "match_all": true }, 21 | { "key": "following_text", "operator": "regex_contains", "operand": "^(?:\t| |\\)|]|\\}|>|$)", "match_all": true }, 22 | { "key": "preceding_text", "operator": "regex_contains", "operand": "b$", "match_all": true }, 23 | { "key": "selector", "operator": "equal", "operand": "source.rust" }, 24 | { "key": "eol_selector", "operator": "not_equal", "operand": "string.quoted.single - punctuation.definition.string.end", "match_all": true } 25 | 26 | ] 27 | }, 28 | // r" will expand to r"" 29 | // b" will expand to b"" 30 | { "keys": ["\""], "command": "insert_snippet", "args": {"contents": "\"$0\""}, "context": 31 | [ 32 | { "key": "setting.auto_match_enabled", "operator": "equal", "operand": true }, 33 | { "key": "selection_empty", "operator": "equal", "operand": true, "match_all": true }, 34 | { "key": "following_text", "operator": "regex_contains", "operand": "^(?:\t| |\\)|]|\\}|>|$)", "match_all": true }, 35 | { "key": "preceding_text", "operator": "regex_contains", "operand": "[rb]$", "match_all": true }, 36 | { "key": "selector", "operator": "equal", "operand": "source.rust" }, 37 | { "key": "eol_selector", "operator": "not_equal", "operand": "string.quoted.double - punctuation.definition.string.end", "match_all": true } 38 | ] 39 | }, 40 | // r#" will expand to r#""# 41 | // Additional # characters will be duplicated on both sides. 42 | { "keys": ["\""], "command": "insert_snippet", "args": {"contents": "\"$0\"${TM_CURRENT_WORD/[^#]+/$1/}"}, "context": 43 | [ 44 | { "key": "setting.auto_match_enabled", "operator": "equal", "operand": true }, 45 | { "key": "selection_empty", "operator": "equal", "operand": true, "match_all": true }, 46 | { "key": "following_text", "operator": "regex_contains", "operand": "^(?:\t| |\\)|]|\\}|>|$)", "match_all": true }, 47 | { "key": "preceding_text", "operator": "regex_contains", "operand": "r#+$", "match_all": true }, 48 | { "key": "selector", "operator": "equal", "operand": "source.rust" }, 49 | { "key": "eol_selector", "operator": "not_equal", "operand": "string.quoted.double - punctuation.definition.string.end", "match_all": true } 50 | ] 51 | }, 52 | // # will skip past the # at the end of r#""# 53 | { "keys": ["#"], "command": "move", "args": {"by": "characters", "forward": true}, "context": 54 | [ 55 | { "key": "setting.auto_match_enabled", "operator": "equal", "operand": true }, 56 | { "key": "selection_empty", "operator": "equal", "operand": true, "match_all": true }, 57 | { "key": "following_text", "operator": "regex_contains", "operand": "^#", "match_all": true }, 58 | { "key": "selector", "operator": "equal", "operand": "source.rust string.quoted.double.raw punctuation.definition.string.end" }, 59 | { "key": "eol_selector", "operator": "not_equal", "operand": "string.quoted.double.raw - punctuation.definition.string.end", "match_all": true }, 60 | ] 61 | }, 62 | ] 63 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy 2 | of this software and associated documentation files (the "Software"), to deal 3 | in the Software without restriction, including without limitation the rights 4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 | copies of the Software, and to permit persons to whom the Software is 6 | furnished to do so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in 9 | all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 17 | THE SOFTWARE. 18 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | 4 | "id": "preferences", 5 | "children": 6 | [ 7 | { 8 | "caption": "Package Settings", 9 | "mnemonic": "P", 10 | "id": "package-settings", 11 | "children": 12 | [ 13 | { 14 | "caption": "Rust Enhanced", 15 | "children": 16 | [ 17 | { 18 | "caption": "Settings", 19 | "command": "edit_settings", 20 | "args": 21 | { 22 | "base_file": "${packages}/Rust Enhanced/RustEnhanced.sublime-settings", 23 | "default": "{\n\t$0\n}\n" 24 | } 25 | } 26 | ] 27 | } 28 | ] 29 | } 30 | ] 31 | } 32 | 33 | ] 34 | -------------------------------------------------------------------------------- /RustComment.JSON-tmPreferences: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Rust Comments", 3 | "scope": "source.rust", 4 | "settings": { 5 | "shellVariables": [{"name": "TM_COMMENT_START", "value": "// "}, 6 | {"name": "TM_COMMENT_START_2", "value": "/*"}, 7 | {"name": "TM_COMMENT_END_2", "value": "*/"}, 8 | {"name": "TM_COMMENT_DISABLE_INDENT", "value": "no"}] 9 | }, 10 | "uuid": "e36d2f49-c7b0-42fe-b902-a0ed36a258ca" 11 | } 12 | -------------------------------------------------------------------------------- /RustComment.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Rust Comments 7 | scope 8 | source.rust 9 | settings 10 | 11 | shellVariables 12 | 13 | 14 | name 15 | TM_COMMENT_START 16 | value 17 | // 18 | 19 | 20 | name 21 | TM_COMMENT_START_2 22 | value 23 | /* 24 | 25 | 26 | name 27 | TM_COMMENT_END_2 28 | value 29 | */ 30 | 31 | 32 | name 33 | TM_COMMENT_DISABLE_INDENT 34 | value 35 | no 36 | 37 | 38 | name 39 | TM_COMMENT_START_3 40 | value 41 | /// 42 | 43 | 44 | name 45 | TM_COMMENT_START_4 46 | value 47 | //! 48 | 49 | 50 | 51 | uuid 52 | e36d2f49-c7b0-42fe-b902-a0ed36a258ca 53 | 54 | 55 | -------------------------------------------------------------------------------- /RustEnhanced.sublime-build: -------------------------------------------------------------------------------- 1 | { 2 | "target": "cargo_exec", 3 | "selector": "source.rust", 4 | "keyfiles": ["Cargo.toml"], 5 | "command": "build", 6 | 7 | "variants": [ 8 | { 9 | "name": "Automatic", 10 | "command": "auto", 11 | }, 12 | { 13 | "name": "Run", 14 | "command": "run", 15 | }, 16 | { 17 | "name": "Run (with args)...", 18 | "command": "run", 19 | "command_info": { 20 | "wants_run_args": true 21 | } 22 | }, 23 | { 24 | "name": "Check", 25 | "command": "check", 26 | }, 27 | { 28 | "name": "Test", 29 | "command": "test", 30 | }, 31 | { 32 | "name": "Test (with args)...", 33 | "command": "test", 34 | "command_info": { 35 | "wants_run_args": true 36 | } 37 | }, 38 | { 39 | "name": "Bench", 40 | "command": "bench", 41 | }, 42 | { 43 | "name": "Clean", 44 | "command": "clean", 45 | }, 46 | { 47 | "name": "Document", 48 | "command": "doc", 49 | }, 50 | { 51 | "name": "Clippy", 52 | "command": "clippy", 53 | }, 54 | { 55 | "name": "Script", 56 | "command": "script", 57 | }, 58 | ] 59 | } 60 | -------------------------------------------------------------------------------- /RustEnhanced.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "Rust: Toggle Syntax Checking", 4 | "command": "toggle_rust_syntax_setting" 5 | }, 6 | { 7 | "caption": "Rust: Next Message", 8 | "command": "rust_next_message" 9 | }, 10 | { 11 | "caption": "Rust: Prev Message", 12 | "command": "rust_prev_message" 13 | }, 14 | { 15 | "caption": "Rust: Cancel Build", 16 | "command": "rust_cancel" 17 | }, 18 | { 19 | "caption": "Rust: Create New Cargo Build Variant", 20 | "command": "cargo_create_new_build" 21 | }, 22 | { 23 | "caption": "Rust: Configure Cargo Build", 24 | "command": "cargo_configure" 25 | }, 26 | { 27 | "caption": "Preferences: Rust Enhanced Settings", 28 | "command": "edit_settings", 29 | "args": 30 | { 31 | "base_file": "${packages}/Rust Enhanced/RustEnhanced.sublime-settings", 32 | "default": "{\n\t$0\n}\n" 33 | } 34 | }, 35 | { 36 | "caption": "Rust: List All Messages", 37 | "command": "rust_list_messages" 38 | }, 39 | { 40 | "caption": "Rust: Run Test At Cursor", 41 | "command": "cargo_test_at_cursor" 42 | }, 43 | { 44 | "caption": "Rust: Run Tests In Current File", 45 | "command": "cargo_test_current_file" 46 | }, 47 | { 48 | "caption": "Rust: Run Benchmark At Cursor", 49 | "command": "cargo_bench_at_cursor" 50 | }, 51 | { 52 | "caption": "Rust: Run Benchmarks In Current File", 53 | "command": "cargo_bench_current_file" 54 | }, 55 | { 56 | "caption": "Rust: Open Debug Log", 57 | "command": "rust_open_log" 58 | }, 59 | { 60 | "caption": "Rust: Open Last Build Output As View", 61 | "command": "rust_show_build_output" 62 | }, 63 | { 64 | "caption": "Rust: Popup Message At Cursor", 65 | "command": "rust_message_popup" 66 | } 67 | ] 68 | -------------------------------------------------------------------------------- /RustEnhanced.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // Enable the on-save syntax checking plugin, which will highlight any 3 | // build errors or warnings. 4 | "rust_syntax_checking": true, 5 | 6 | // The method used to check code on-save. 7 | // "check" or "clippy" (see README.md) 8 | "rust_syntax_checking_method": "check", 9 | 10 | // Enable checking of test code within #[cfg(test)] sections. 11 | // `check` method requires Rust 1.23 or newer. 12 | "rust_syntax_checking_include_tests": true, 13 | 14 | // If true, will not display warning messages. 15 | "rust_syntax_hide_warnings": false, 16 | 17 | // Color of messages for "clear" theme. 18 | // These use CSS colors. See 19 | // https://www.sublimetext.com/docs/3/minihtml.html for more detail. 20 | "rust_syntax_error_color": "var(--redish)", 21 | "rust_syntax_warning_color": "var(--yellowish)", 22 | "rust_syntax_note_color": "var(--greenish)", 23 | "rust_syntax_help_color": "var(--bluish)", 24 | 25 | // Specify environment variables to add when running Cargo. 26 | // "rust_env": {"PATH": "$PATH:$HOME/.cargo/bin"} 27 | 28 | // If true, will use the environment from the user's login shell when 29 | // running Cargo. 30 | "rust_include_shell_env": true, 31 | 32 | // For errors/warnings, how to show the inline message. 33 | // "normal" - Shows the message inline. 34 | // "popup" - Show popup on mouse hover. 35 | // "none" - Do not show the message inline. 36 | "rust_phantom_style": "normal", 37 | 38 | // For errors/warnings, how to highlight the region of the error. 39 | // "outline" - Outlines the region. 40 | // "solid_underline" - A solid underline. 41 | // "stippled_underline" - A stippled underline. 42 | // "squiggly_underline" - A squiggly underline. 43 | // "none" - No outlining. 44 | "rust_region_style": "outline", 45 | 46 | // For errors/warnings, what kind of icon to use in the gutter. 47 | // "shape" - Shape-based icons. 48 | // "circle" - Simple circle icons. 49 | // "none" - Do not place icons in the gutter. 50 | "rust_gutter_style": "shape", 51 | 52 | // Style for displaying inline messages. Can be: 53 | // "clear" - Clear background with colors matching your color scheme. 54 | // "solid" - Solid background color. 55 | "rust_message_theme": "clear", 56 | 57 | // If `true`, displays diagnostic messages under the cursor in the status bar. 58 | "rust_message_status_bar": false, 59 | 60 | // The message to display when the syntax check is running. 61 | "rust_message_status_bar_msg": "Rust check running", 62 | 63 | // A list of chars that cycle through in the status bar to indicate progress. 64 | "rust_message_status_bar_chars": [".", "..", "...", ".."], 65 | 66 | // How often (ms) should the status bar text be updated when syntax checking. 67 | "rust_message_status_bar_update_delay": 200, 68 | 69 | // If your cargo project has several build targets, it's possible to specify mapping of 70 | // source code filenames to the target names to enable syntax checking. 71 | // "projects": { 72 | // // One entry per project. Key is a project name. 73 | // "my_cool_stuff": { 74 | // "root": "/path/to/my/cool/stuff/project", // without trailing /src 75 | // // Targets will be used to replace {target} part in the syntax check command: 76 | // // command = 'cargo rustc {target} -- '. If no one target matches 77 | // // an empty string will be used. 78 | // "targets": { 79 | // "bin/foo.rs": "--bin foo", // format is "source_code_filename -> target_name" 80 | // "bin/bar.rs": "--bin bar", 81 | // "_default": "--lib" // or "--bin main" 82 | // } 83 | // } 84 | // } 85 | "auto_complete_triggers": 86 | [ 87 | { 88 | "characters": ".:", 89 | "selector": "source.rust" 90 | } 91 | ] 92 | } 93 | -------------------------------------------------------------------------------- /RustIndent.JSON-tmPreferences: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Rust Indent", 3 | "scope": "source.rust", 4 | "settings": { 5 | "increaseIndentPattern": "^.*\\{[^}\"']*$", 6 | "decreaseIndentPattern": "^(.*\\*/)?\\s*\\}.*$" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /RustIndent.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Rust Indent 7 | scope 8 | source.rust 9 | settings 10 | 11 | decreaseIndentPattern 12 | ^(.*\*/)?\s*\}.*$ 13 | increaseIndentPattern 14 | ^.*\{[^}"']*$ 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /RustSymbols.JSON-tmPreferences: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Rust Symbols", 3 | "scope": "entity.name.function.rust, entity.name.macro.rust, entity.name.struct.rust, entity.name.enum.rust, entity.name.module.rust, entity.name.type.rust, entity.name.trait.rust, entity.name.constant.rust, entity.name.union.rust", 4 | "settings": { 5 | "showInSymbolList": 1, 6 | "showInIndexedSymbolList": 1 7 | }, 8 | "uuid": "d3270dd1-4ccd-428e-8dda-d3d20ee9fc7e" 9 | } 10 | -------------------------------------------------------------------------------- /RustSymbols.tmPreferences: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | name 6 | Rust Symbols 7 | scope 8 | entity.name.function.rust, entity.name.macro.rust, entity.name.struct.rust, entity.name.enum.rust, entity.name.module.rust, entity.name.type.rust, entity.name.trait.rust, entity.name.constant.rust, entity.name.union.rust 9 | settings 10 | 11 | showInIndexedSymbolList 12 | 1 13 | showInSymbolList 14 | 1 15 | 16 | uuid 17 | d3270dd1-4ccd-428e-8dda-d3d20ee9fc7e 18 | 19 | 20 | -------------------------------------------------------------------------------- /SyntaxCheckPlugin.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import sublime_plugin 3 | import os 4 | import time 5 | from .rust import (messages, rust_proc, rust_thread, util, target_detect, 6 | cargo_settings, semver, log) 7 | 8 | 9 | """On-save syntax checking. 10 | 11 | This contains the code for displaying message phantoms for errors/warnings 12 | whenever you save a Rust file. 13 | """ 14 | 15 | 16 | # TODO: Use ViewEventListener if 17 | # https://github.com/SublimeTextIssues/Core/issues/2411 is fixed. 18 | class RustSyntaxCheckEvent(sublime_plugin.EventListener): 19 | 20 | last_save = 0 21 | 22 | def on_post_save(self, view): 23 | enabled = util.get_setting('rust_syntax_checking', True) 24 | if not enabled or not util.active_view_is_rust(view=view): 25 | return 26 | prev_save = self.last_save 27 | self.last_save = time.time() 28 | if self.last_save - prev_save < 0.25: 29 | # This is a guard for a few issues. 30 | # * `on_post_save` gets called multiple times if the same buffer 31 | # is opened in multiple views (with the same view passed in each 32 | # time). See: 33 | # https://github.com/SublimeTextIssues/Core/issues/289 34 | # * When using "Save All" we want to avoid launching a bunch of 35 | # threads and then immediately killing them. 36 | return 37 | log.clear_log(view.window()) 38 | messages.erase_status(view) 39 | t = RustSyntaxCheckThread(view) 40 | t.start() 41 | 42 | 43 | class RustSyntaxCheckThread(rust_thread.RustThread, rust_proc.ProcListener): 44 | 45 | # Thread name. 46 | name = 'Syntax Check' 47 | # The Sublime view that triggered the check. 48 | view = None 49 | # The Sublime window that triggered the check. 50 | window = None 51 | # Absolute path to the view that triggered the check. 52 | triggered_file_name = None 53 | # Directory where cargo will be run. 54 | cwd = None 55 | # Base path for relative paths in messages. 56 | msg_rel_path = None 57 | # This flag is used to terminate early. In situations where we can't 58 | # auto-detect the appropriate Cargo target, we compile multiple targets. 59 | # If we receive any messages for the current view, we might as well stop. 60 | # Otherwise, you risk displaying duplicate messages for shared modules. 61 | this_view_found = False 62 | # The path to the top-level Cargo target filename (like main.rs or 63 | # lib.rs). 64 | current_target_src = None 65 | done = False 66 | 67 | def __init__(self, view): 68 | self.view = view 69 | self.window = view.window() 70 | self.rendered = [] 71 | super(RustSyntaxCheckThread, self).__init__(view.window()) 72 | 73 | def run(self): 74 | self.triggered_file_name = os.path.abspath(self.view.file_name()) 75 | self.cwd = util.find_cargo_manifest(self.triggered_file_name) 76 | if self.cwd is None: 77 | # A manifest is required. 78 | log.critical(self.window, util.multiline_fix(""" 79 | Rust Enhanced skipping on-save syntax check. 80 | Failed to find Cargo.toml from %r 81 | A Cargo.toml manifest is required. 82 | """), self.triggered_file_name) 83 | return 84 | 85 | self.update_status() 86 | self.this_view_found = False 87 | CHECK_FAIL_MSG = 'Rust check failed, see console or debug log.' 88 | try: 89 | messages.clear_messages(self.window) 90 | try: 91 | rc = self.get_rustc_messages() 92 | except rust_proc.ProcessTerminatedError: 93 | self.window.status_message('') 94 | return 95 | except Exception as e: 96 | self.window.status_message(CHECK_FAIL_MSG) 97 | raise 98 | finally: 99 | self.done = True 100 | messages.messages_finished(self.window) 101 | counts = messages.message_counts(self.window) 102 | if counts: 103 | msg = [] 104 | for key, value in sorted(counts.items(), key=lambda x: x[0]): 105 | level = key.plural if value > 1 else key.name 106 | msg.append('%i %s' % (value, level)) 107 | self.window.status_message('Rust check: %s' % (', '.join(msg,))) 108 | elif rc: 109 | self.window.status_message(CHECK_FAIL_MSG) 110 | else: 111 | self.window.status_message('Rust check: success') 112 | 113 | def update_status(self, count=0): 114 | if self.done: 115 | return 116 | 117 | status_msg = util.get_setting('rust_message_status_bar_msg') 118 | status_chars = util.get_setting('rust_message_status_bar_chars') 119 | status_update_delay = util.get_setting('rust_message_status_bar_update_delay') 120 | 121 | try: 122 | status_chars_len = len(status_chars) 123 | num = count % status_chars_len 124 | if num == status_chars_len - 1: 125 | num = -1 126 | num += 1 127 | 128 | self.window.status_message(status_msg + status_chars[num]) 129 | sublime.set_timeout(lambda: self.update_status(count + 1), status_update_delay) 130 | except Exception as e: 131 | self.window.status_message('Error setting status text!') 132 | log.critical(self.window, "An error occurred setting status text: " + str(e)) 133 | 134 | def get_rustc_messages(self): 135 | """Top-level entry point for generating messages for the given 136 | filename. 137 | 138 | :raises rust_proc.ProcessTerminatedError: Check was canceled. 139 | :raises OSError: Failed to launch the child process. 140 | 141 | :returns: Returns the process return code. 142 | """ 143 | method = util.get_setting('rust_syntax_checking_method', 'check') 144 | settings = cargo_settings.CargoSettings(self.window) 145 | settings.load() 146 | command_info = cargo_settings.CARGO_COMMANDS[method] 147 | 148 | if method == 'no-trans': 149 | print('rust_syntax_checking_method == "no-trans" is no longer supported.') 150 | print('Please change the config setting to "check".') 151 | method = 'check' 152 | 153 | if method not in ['check', 'clippy']: 154 | print('Unknown setting for `rust_syntax_checking_method`: %r' % (method,)) 155 | return -1 156 | 157 | # Try to grab metadata only once. `target` is None since that's what 158 | # we're trying to figure out. 159 | toolchain = settings.get_computed(self.cwd, method, None, 'toolchain') 160 | metadata = util.get_cargo_metadata(self.window, self.cwd, toolchain=toolchain) 161 | if not metadata: 162 | return -1 163 | td = target_detect.TargetDetector(self.window) 164 | targets = td.determine_targets(self.triggered_file_name, metadata=metadata) 165 | if not targets: 166 | return -1 167 | rc = 0 168 | for (target_src, target_args) in targets: 169 | cmd = settings.get_command(method, command_info, self.cwd, self.cwd, 170 | initial_settings={'target': ' '.join(target_args)}, 171 | force_json=True, metadata=metadata) 172 | self.msg_rel_path = cmd['msg_rel_path'] 173 | if (util.get_setting('rust_syntax_checking_include_tests', True) and 174 | semver.match(cmd['rustc_version'], '>=1.23.0')): 175 | # Including the test harness has a few drawbacks. 176 | # missing_docs lint is disabled (see 177 | # https://github.com/rust-lang/sublime-rust/issues/156) 178 | # It also disables the "main function not found" error for 179 | # binaries. 180 | cmd['command'].append('--profile=test') 181 | p = rust_proc.RustProc() 182 | self.current_target_src = target_src 183 | p.run(self.window, cmd['command'], self.cwd, self, env=cmd['env']) 184 | rc = p.wait() 185 | if self.this_view_found: 186 | return rc 187 | return rc 188 | 189 | ######################################################################### 190 | # ProcListner methods 191 | ######################################################################### 192 | 193 | def on_begin(self, proc): 194 | self.rendered.append('[Running: %s]' % (' '.join(proc.cmd),)) 195 | 196 | def on_data(self, proc, data): 197 | log.log(self.window, data) 198 | self.rendered.append(data) 199 | 200 | def on_error(self, proc, message): 201 | log.critical(self.window, 'Rust Error: %s', message) 202 | self.rendered.append(message) 203 | 204 | def on_json(self, proc, obj): 205 | try: 206 | message = obj['message'] 207 | except KeyError: 208 | return 209 | messages.add_rust_messages(self.window, self.msg_rel_path, message, 210 | self.current_target_src, msg_cb=None) 211 | if messages.has_message_for_path(self.window, 212 | self.triggered_file_name): 213 | self.this_view_found = True 214 | try: 215 | self.rendered.append(message['rendered']) 216 | except KeyError: 217 | pass 218 | 219 | def on_finished(self, proc, rc): 220 | log.log(self.window, 'On-save check finished.') 221 | # TODO: Also put message in self.rendered about [Finished in …] 222 | # TODO: Figure out how to share all this code between here and opanel 223 | win_info = messages.get_or_init_window_info(self.window) 224 | win_info['rendered'] = ''.join(self.rendered) 225 | 226 | def on_terminated(self, proc): 227 | log.log(self.window, 'Process Interrupted') 228 | -------------------------------------------------------------------------------- /cargo_build.py: -------------------------------------------------------------------------------- 1 | """Sublime commands for the cargo build system.""" 2 | 3 | import functools 4 | import sublime 5 | import sublime_plugin 6 | import sys 7 | from .rust import (rust_proc, rust_thread, opanel, util, messages, 8 | cargo_settings, target_detect) 9 | from .rust.cargo_config import * 10 | from .rust.log import (log, clear_log, RustOpenLog, RustLogEvent) 11 | 12 | # Maps command to an input string. Used to pre-populate the input panel with 13 | # the last entered value. 14 | LAST_EXTRA_ARGS = {} 15 | 16 | 17 | class CargoExecCommand(sublime_plugin.WindowCommand): 18 | 19 | """cargo_exec Sublime command. 20 | 21 | This takes the following arguments: 22 | 23 | - `command`: The command name to run. Commands are defined in the 24 | `cargo_settings` module. You can define your own custom command by 25 | passing in `command_info`. 26 | - `command_info`: Dictionary of values the defines how the cargo command 27 | is constructed. See `cargo_settings.CARGO_COMMANDS`. 28 | - `settings`: Dictionary of settings overriding anything set in the 29 | Sublime project settings (see `cargo_settings` module). 30 | """ 31 | 32 | # The combined command info from `cargo_settings` and whatever the user 33 | # passed in. 34 | command_info = None 35 | # Dictionary of initial settings passed in by the user. 36 | initial_settings = None 37 | # CargoSettings instance. 38 | settings = None 39 | # Directory where to run the command. 40 | working_dir = None 41 | # Path used for the settings key. This is typically `working_dir` 42 | # (previously was used for script paths, now unused). 43 | settings_path = None 44 | 45 | def run(self, command=None, command_info=None, settings=None): 46 | if command is None: 47 | return self.window.run_command('build', {'select': True}) 48 | clear_log(self.window) 49 | self.initial_settings = settings if settings else {} 50 | self.settings = cargo_settings.CargoSettings(self.window) 51 | self.settings.load() 52 | if command == 'auto': 53 | self._detect_auto_build() 54 | else: 55 | self.command_name = command 56 | self.command_info = cargo_settings.CARGO_COMMANDS\ 57 | .get(command, {}).copy() 58 | if command_info: 59 | self.command_info.update(command_info) 60 | self._determine_working_path(self._run_check_for_args) 61 | 62 | def _detect_auto_build(self): 63 | """Handle the "auto" build variant, which automatically picks a build 64 | command based on the current view.""" 65 | if not util.active_view_is_rust(): 66 | sublime.error_message(util.multiline_fix(""" 67 | Error: Could not determine what to build. 68 | 69 | Open a Rust source file as the active Sublime view. 70 | """)) 71 | return 72 | td = target_detect.TargetDetector(self.window) 73 | view = self.window.active_view() 74 | targets = td.determine_targets(view.file_name()) 75 | if len(targets) == 0: 76 | sublime.error_message(util.multiline_fix(""" 77 | Error: Could not determine what to build. 78 | 79 | Try using one of the explicit build variants. 80 | """)) 81 | return 82 | 83 | elif len(targets) == 1: 84 | self._auto_choice_made(targets, 0) 85 | 86 | else: 87 | # Can't determine a single target, let the user choose one. 88 | targets.sort() 89 | display_items = [' '.join(x[1]) for x in targets] 90 | on_done = functools.partial(self._auto_choice_made, targets) 91 | self.window.show_quick_panel(display_items, on_done) 92 | 93 | def _auto_choice_made(self, targets, index): 94 | if index != -1: 95 | src_path, cmd_line = targets[index] 96 | actions = { 97 | '--bin': 'run', 98 | '--example': 'run', 99 | '--lib': 'build', 100 | '--bench': 'bench', 101 | '--test': 'test', 102 | } 103 | cmd = actions[cmd_line[0]] 104 | self.initial_settings['target'] = ' '.join(cmd_line) 105 | self.run(command=cmd, settings=self.initial_settings) 106 | 107 | def _determine_working_path(self, on_done): 108 | """Determine where Cargo should be run. 109 | 110 | This may trigger some Sublime user interaction if necessary. 111 | """ 112 | working_dir = self.initial_settings.get('working_dir') 113 | if working_dir: 114 | self.working_dir = working_dir 115 | self.settings_path = working_dir 116 | return on_done() 117 | 118 | script_path = self.initial_settings.get('script_path') 119 | if script_path: 120 | self.working_dir = os.path.dirname(script_path) 121 | self.settings_path = script_path 122 | return on_done() 123 | 124 | default_path = self.settings.get_project_base('default_path') 125 | if default_path: 126 | self.settings_path = default_path 127 | if os.path.isfile(default_path): 128 | self.working_dir = os.path.dirname(default_path) 129 | else: 130 | self.working_dir = default_path 131 | return on_done() 132 | 133 | if self.command_info.get('requires_manifest', True): 134 | cmd = CargoConfigPackage(self.window) 135 | cmd.run(functools.partial(self._on_manifest_choice, on_done)) 136 | else: 137 | # For now, assume you need a Rust file if not needing a manifest 138 | # (previously used for `cargo script`, now unused). 139 | view = self.window.active_view() 140 | if util.active_view_is_rust(view=view): 141 | self.settings_path = view.file_name() 142 | self.working_dir = os.path.dirname(self.settings_path) 143 | return on_done() 144 | else: 145 | sublime.error_message(util.multiline_fix(""" 146 | Error: Could not determine what Rust source file to use. 147 | 148 | Open a Rust source file as the active Sublime view.""")) 149 | return 150 | 151 | def _on_manifest_choice(self, on_done, package_path): 152 | self.settings_path = package_path 153 | self.working_dir = package_path 154 | on_done() 155 | 156 | def _run_check_for_args(self): 157 | if self.command_info.get('wants_run_args', False) and \ 158 | not self.initial_settings.get('extra_run_args'): 159 | self.window.show_input_panel('Enter extra args:', 160 | LAST_EXTRA_ARGS.get(self.command_name, ''), 161 | self._on_extra_args, None, None) 162 | else: 163 | self._run() 164 | 165 | def _on_extra_args(self, args): 166 | LAST_EXTRA_ARGS[self.command_info['command']] = args 167 | self.initial_settings['extra_run_args'] = args 168 | self._run() 169 | 170 | def _run(self): 171 | t = CargoExecThread(self.window, self.settings, 172 | self.command_name, self.command_info, 173 | self.initial_settings, 174 | self.settings_path, self.working_dir) 175 | t.start() 176 | 177 | 178 | class CargoExecThread(rust_thread.RustThread): 179 | 180 | silently_interruptible = False 181 | name = 'Cargo Exec' 182 | 183 | def __init__(self, window, settings, 184 | command_name, command_info, 185 | initial_settings, settings_path, working_dir): 186 | super(CargoExecThread, self).__init__(window) 187 | self.settings = settings 188 | self.command_name = command_name 189 | self.command_info = command_info 190 | self.initial_settings = initial_settings 191 | self.settings_path = settings_path 192 | self.working_dir = working_dir 193 | 194 | def run(self): 195 | cmd = self.settings.get_command(self.command_name, 196 | self.command_info, 197 | self.settings_path, 198 | self.working_dir, 199 | self.initial_settings) 200 | if not cmd: 201 | return 202 | messages.clear_messages(self.window) 203 | p = rust_proc.RustProc() 204 | listener = opanel.OutputListener(self.window, cmd['msg_rel_path'], 205 | self.command_name, 206 | cmd['rustc_version']) 207 | decode_json = util.get_setting('show_errors_inline', True) and \ 208 | self.command_info.get('allows_json', False) 209 | try: 210 | p.run(self.window, cmd['command'], 211 | self.working_dir, listener, 212 | env=cmd['env'], 213 | decode_json=decode_json, 214 | json_stop_pattern=self.command_info.get('json_stop_pattern')) 215 | p.wait() 216 | except rust_proc.ProcessTerminatedError: 217 | return 218 | 219 | 220 | # This is used by the test code. Due to the async nature of the on_load event, 221 | # it can cause problems with the rapid loading of views. 222 | ON_LOAD_MESSAGES_ENABLED = True 223 | 224 | 225 | class MessagesViewEventListener(sublime_plugin.ViewEventListener): 226 | 227 | """Every time a new file is loaded, check if is a Rust file with messages, 228 | and if so, display the messages. 229 | """ 230 | 231 | @classmethod 232 | def is_applicable(cls, settings): 233 | return ON_LOAD_MESSAGES_ENABLED and util.is_rust_view(settings) 234 | 235 | @classmethod 236 | def applies_to_primary_view_only(cls): 237 | return False 238 | 239 | def on_load_async(self): 240 | messages.show_messages_for_view(self.view) 241 | 242 | 243 | class NextPrevBase(sublime_plugin.WindowCommand): 244 | 245 | def _has_inline(self): 246 | try: 247 | return messages.WINDOW_MESSAGES[self.window.id()]['has_inline'] 248 | except KeyError: 249 | return False 250 | 251 | 252 | class RustNextMessageCommand(NextPrevBase): 253 | 254 | def run(self, levels='all'): 255 | if self._has_inline(): 256 | messages.show_next_message(self.window, levels) 257 | else: 258 | self.window.run_command('next_result') 259 | 260 | 261 | class RustPrevMessageCommand(NextPrevBase): 262 | 263 | def run(self, levels='all'): 264 | if self._has_inline(): 265 | messages.show_prev_message(self.window, levels) 266 | else: 267 | self.window.run_command('prev_result') 268 | 269 | 270 | class RustCancelCommand(sublime_plugin.WindowCommand): 271 | 272 | def run(self): 273 | try: 274 | t = rust_thread.THREADS[self.window.id()] 275 | except KeyError: 276 | pass 277 | else: 278 | t.terminate() 279 | # Also call Sublime's cancel command, in case the user is using a 280 | # normal Sublime build. 281 | self.window.run_command('cancel_build') 282 | 283 | 284 | class RustDismissMessagesCommand(sublime_plugin.WindowCommand): 285 | 286 | """Removes all inline messages.""" 287 | 288 | def run(self): 289 | messages.clear_messages(self.window, soft=True) 290 | 291 | 292 | class RustListMessagesCommand(sublime_plugin.WindowCommand): 293 | 294 | """Shows a quick panel with a list of all messages.""" 295 | 296 | def run(self): 297 | messages.list_messages(self.window) 298 | 299 | 300 | # Patterns used to help find test function names. 301 | # This is far from perfect, but should be good enough. 302 | SPACE = r'[ \t]' 303 | OPT_COMMENT = r"""(?: 304 | (?: [ \t]* //.*) 305 | | (?: [ \t]* /\*.*\*/ [ \t]* ) 306 | )?""" 307 | IDENT = r"""(?: 308 | [a-z A-Z] [a-z A-Z 0-9 _]* 309 | | _ [a-z A-Z 0-9 _]+ 310 | )""" 311 | TEST_PATTERN = r"""(?x) 312 | {SPACE}* \# {SPACE}* \[ {SPACE}* {WHAT} {SPACE}* \] {SPACE}* 313 | (?: 314 | (?: {SPACE}* \#\[ [^]]+ \] {OPT_COMMENT} \n ) 315 | | (?: {OPT_COMMENT} \n ) 316 | )* 317 | .* fn {SPACE}+ ({IDENT}+) 318 | """ 319 | 320 | 321 | def _target_to_test(what, view, on_done): 322 | """Helper used to determine build target from given view.""" 323 | td = target_detect.TargetDetector(view.window()) 324 | targets = td.determine_targets(view.file_name()) 325 | if len(targets) == 0: 326 | sublime.error_message('Error: Could not determine target to %s.' % what) 327 | elif len(targets) == 1: 328 | on_done(' '.join(targets[0][1])) 329 | else: 330 | # Can't determine a single target, let the user choose one. 331 | display_items = [' '.join(x[1]) for x in targets] 332 | 333 | def quick_on_done(idx): 334 | on_done(targets[idx][1]) 335 | 336 | view.window().show_quick_panel(display_items, quick_on_done) 337 | 338 | 339 | def _pt_to_test_name(what, pt, view): 340 | """Helper used to convert Sublime point to a test/bench function name.""" 341 | fn_names = [] 342 | pat = TEST_PATTERN.format(WHAT=what, **globals()) 343 | regions = view.find_all(pat, 0, r'\1', fn_names) 344 | if not regions: 345 | sublime.error_message('Could not find a Rust %s function.' % what) 346 | return None 347 | # Assuming regions are in ascending order. 348 | indices = [i for (i, r) in enumerate(regions) if r.a <= pt] 349 | if not indices: 350 | sublime.error_message('No %s functions found about the current point.' % what) 351 | return None 352 | return fn_names[indices[-1]] 353 | 354 | 355 | def _cargo_test_pt(what, pt, view): 356 | """Helper used to run a test for a given point in the given view.""" 357 | def do_test(target): 358 | test_fn_name = _pt_to_test_name(what, pt, view) 359 | if test_fn_name: 360 | view.window().run_command('cargo_exec', args={ 361 | 'command': what, 362 | 'settings': { 363 | 'target': target, 364 | 'extra_run_args': '--exact ' + test_fn_name 365 | } 366 | }) 367 | 368 | _target_to_test(what, view, do_test) 369 | 370 | 371 | class CargoHere(sublime_plugin.WindowCommand): 372 | 373 | """Base class for mouse-here commands. 374 | 375 | Subclasses set `what` attribute. 376 | """ 377 | 378 | what = None 379 | 380 | def run(self, event): 381 | view = self.window.active_view() 382 | if not view: 383 | return 384 | pt = view.window_to_text((event['x'], event['y'])) 385 | _cargo_test_pt(self.what, pt, view) 386 | 387 | def want_event(self): 388 | return True 389 | 390 | 391 | class CargoTestHereCommand(CargoHere): 392 | 393 | """Determines the test name at the current mouse position, and runs just 394 | that test.""" 395 | 396 | what = 'test' 397 | 398 | 399 | class CargoBenchHereCommand(CargoHere): 400 | 401 | """Determines the benchmark at the current mouse position, and runs just 402 | that benchmark.""" 403 | 404 | what = 'bench' 405 | 406 | 407 | class CargoTestAtCursorCommand(sublime_plugin.TextCommand): 408 | 409 | """Determines the test name at the current cursor position, and runs just 410 | that test.""" 411 | 412 | def run(self, edit): 413 | pt = self.view.sel()[0].begin() 414 | _cargo_test_pt('test', pt, self.view) 415 | 416 | 417 | class CargoCurrentFile(sublime_plugin.WindowCommand): 418 | 419 | """Base class for current file commands. 420 | 421 | Subclasses set `what` attribute. 422 | """ 423 | 424 | what = None 425 | 426 | def run(self): 427 | def _test_file(target): 428 | self.window.run_command('cargo_exec', args={ 429 | 'command': self.what, 430 | 'settings': { 431 | 'target': target 432 | } 433 | }) 434 | 435 | view = self.window.active_view() 436 | _target_to_test(self.what, view, _test_file) 437 | 438 | 439 | class CargoTestCurrentFileCommand(CargoCurrentFile): 440 | 441 | """Runs all tests in the current file.""" 442 | 443 | what = 'test' 444 | 445 | 446 | class CargoBenchCurrentFileCommand(CargoCurrentFile): 447 | 448 | """Runs all benchmarks in the current file.""" 449 | 450 | what = 'bench' 451 | 452 | 453 | class CargoRunCurrentFileCommand(CargoCurrentFile): 454 | 455 | """Runs the current file.""" 456 | 457 | what = 'run' 458 | 459 | 460 | class CargoBenchAtCursorCommand(sublime_plugin.TextCommand): 461 | 462 | """Determines the benchmark name at the current cursor position, and runs 463 | just that benchmark.""" 464 | 465 | def run(self, edit): 466 | pt = self.view.sel()[0].begin() 467 | _cargo_test_pt('bench', pt, self.view) 468 | 469 | 470 | class CargoMessageHover(sublime_plugin.ViewEventListener): 471 | 472 | """Displays a popup if `rust_phantom_style` is "popup" when the mouse 473 | hovers over a message region. 474 | 475 | Limitation: If you edit the file and shift the region, the hover feature 476 | will not recognize the new region. This means that the popup will only 477 | show in the old location. 478 | """ 479 | 480 | @classmethod 481 | def is_applicable(cls, settings): 482 | return util.is_rust_view(settings) 483 | 484 | @classmethod 485 | def applies_to_primary_view_only(cls): 486 | return False 487 | 488 | def on_hover(self, point, hover_zone): 489 | if util.get_setting('rust_phantom_style', 'normal') == 'popup': 490 | messages.message_popup(self.view, point, hover_zone) 491 | 492 | 493 | class RustMessagePopupCommand(sublime_plugin.TextCommand): 494 | 495 | """Manually display a popup for any message under the cursor.""" 496 | 497 | def run(self, edit): 498 | for r in self.view.sel(): 499 | messages.message_popup(self.view, r.begin(), sublime.HOVER_TEXT) 500 | 501 | 502 | class RustMessageStatus(sublime_plugin.ViewEventListener): 503 | 504 | """Display message under cursor in status bar.""" 505 | 506 | @classmethod 507 | def is_applicable(cls, settings): 508 | return (util.is_rust_view(settings) 509 | and util.get_setting('rust_message_status_bar', False)) 510 | 511 | @classmethod 512 | def applies_to_primary_view_only(cls): 513 | return False 514 | 515 | def on_selection_modified_async(self): 516 | # https://github.com/SublimeTextIssues/Core/issues/289 517 | # Only works with the primary view, get the correct view. 518 | # (Also called for each view, unfortunately.) 519 | active_view = self.view.window().active_view() 520 | if active_view and active_view.buffer_id() == self.view.buffer_id(): 521 | view = active_view 522 | else: 523 | view = self.view 524 | messages.update_status(view) 525 | 526 | 527 | class RustShowBuildOutput(sublime_plugin.WindowCommand): 528 | 529 | """Opens a view with the rustc-rendered compiler output.""" 530 | 531 | def run(self): 532 | view = self.window.new_file() 533 | view.set_scratch(True) 534 | view.set_name('Rust Enhanced Build Output') 535 | view.assign_syntax('Cargo.sublime-syntax') 536 | win_info = messages.get_or_init_window_info(self.window) 537 | output = win_info['rendered'] 538 | if output == '': 539 | output = "No output available for this window." 540 | view.run_command('append', {'characters': output}) 541 | 542 | 543 | class RustEventListener(sublime_plugin.EventListener): 544 | 545 | def on_activated_async(self, view): 546 | # This is a workaround for this bug: 547 | # https://github.com/SublimeTextIssues/Core/issues/2411 548 | # It would be preferable to use ViewEventListener, but it doesn't work 549 | # on duplicate views created with Goto Anything. 550 | def activate(): 551 | if not util.active_view_is_rust(view=view): 552 | return 553 | if util.get_setting('rust_message_status_bar', False): 554 | messages.update_status(view) 555 | messages.draw_regions_if_missing(view) 556 | 557 | # For some reason, view.window() sometimes returns None here. 558 | # Use set_timeout to give it time to attach to a window. 559 | sublime.set_timeout(activate, 1) 560 | 561 | def on_query_context(self, view, key, operator, operand, match_all): 562 | # Used by the Escape-key keybinding to dismiss inline phantoms. 563 | if key == 'rust_has_messages': 564 | try: 565 | winfo = messages.WINDOW_MESSAGES[view.window().id()] 566 | has_messages = not winfo['hidden'] 567 | except KeyError: 568 | has_messages = False 569 | if operator == sublime.OP_EQUAL: 570 | return operand == has_messages 571 | elif operator == sublime.OP_NOT_EQUAL: 572 | return operand != has_messages 573 | return None 574 | 575 | 576 | class RustAcceptSuggestedReplacement(sublime_plugin.TextCommand): 577 | 578 | """Used for suggested replacements issued by the compiler to apply the 579 | suggested replacement. 580 | """ 581 | 582 | def run(self, edit, region, replacement): 583 | region = sublime.Region(*region) 584 | self.view.replace(edit, region, replacement) 585 | 586 | 587 | class RustScrollToRegion(sublime_plugin.TextCommand): 588 | 589 | """Internal command used to scroll a view to a region.""" 590 | 591 | def run(self, edit, region): 592 | r = sublime.Region(*region) 593 | self.view.sel().clear() 594 | self.view.sel().add(r) 595 | self.view.show_at_center(r) 596 | 597 | 598 | def plugin_unloaded(): 599 | messages.clear_all_messages() 600 | try: 601 | from package_control import events 602 | except ImportError: 603 | return 604 | package_name = __package__.split('.')[0] 605 | if events.pre_upgrade(package_name): 606 | # When upgrading the package, Sublime currently does not cleanly 607 | # unload the `rust` Python package. This is a workaround to ensure 608 | # that it gets completely unloaded so that when it upgrades it will 609 | # load the new package. See 610 | # https://github.com/SublimeTextIssues/Core/issues/2207 611 | re_keys = [key for key in sys.modules if key.startswith(package_name + '.rust')] 612 | for key in re_keys: 613 | del sys.modules[key] 614 | if package_name in sys.modules: 615 | del sys.modules[package_name] 616 | 617 | 618 | def plugin_loaded(): 619 | try: 620 | from package_control import events 621 | except ImportError: 622 | return 623 | package_name = __package__.split('.')[0] 624 | if events.install(package_name): 625 | # Update the syntax for any open views. 626 | for window in sublime.windows(): 627 | for view in window.views(): 628 | fname = view.file_name() 629 | if fname and fname.endswith('.rs'): 630 | view.settings().set('syntax', 631 | 'Packages/%s/RustEnhanced.sublime-syntax' % (package_name,)) 632 | 633 | # Disable the built-in Rust package. 634 | settings = sublime.load_settings('Preferences.sublime-settings') 635 | ignored = settings.get('ignored_packages', []) 636 | if 'Rust' not in ignored: 637 | ignored.append('Rust') 638 | settings.set('ignored_packages', ignored) 639 | sublime.save_settings('Preferences.sublime-settings') 640 | -------------------------------------------------------------------------------- /changelog/2.11.0.md: -------------------------------------------------------------------------------- 1 | # Rust Enhanced 2.11.0 2 | 3 | You must restart Sublime after installing this update. 4 | 5 | ## New Features 6 | - Added `"rust_message_theme"` configuration setting for choosing different 7 | styles of inline messages. Currently two options are available: "clear" and 8 | "solid". See 9 | https://github.com/rust-lang/rust-enhanced/blob/master/docs/build.md#general-settings 10 | for examples. 11 | 12 | - If the Rust compiler provides a suggestion on how to fix an error, the 13 | inline messages now include a link that you can click to automatically apply 14 | the suggestion. 15 | 16 | ## Syntax Updates 17 | - Support u128/i128 integer suffix. 18 | -------------------------------------------------------------------------------- /ci/install-rust.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Install/update rust. 3 | # The first argument should be the toolchain to install. 4 | 5 | set -ex 6 | if [ -z "$1" ] 7 | then 8 | echo "First parameter must be toolchain to install." 9 | exit 1 10 | fi 11 | TOOLCHAIN="$1" 12 | 13 | if ! [ -x "$(command -v rustup)" ] 14 | then 15 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | bash -s -- -y --default-toolchain none 16 | source $HOME/.cargo/env 17 | fi 18 | if [ "$(uname -s)" == "Linux" ] && ! [ -x "$(command -v cc)" ] 19 | then 20 | apt-get update 21 | apt-get -y install --no-install-recommends build-essential 22 | fi 23 | rustup set profile minimal 24 | rustup component remove --toolchain=$TOOLCHAIN rust-docs || echo "already removed" 25 | rustup update --no-self-update $TOOLCHAIN 26 | rustup default $TOOLCHAIN 27 | rustup component add clippy 28 | rustup component add rust-src 29 | rustup -V 30 | rustc -Vv 31 | cargo -V 32 | -------------------------------------------------------------------------------- /dependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "*": { 3 | "*": [ 4 | "shellenv" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /images/gutter/circle-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/circle-error.png -------------------------------------------------------------------------------- /images/gutter/circle-error@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/circle-error@2x.png -------------------------------------------------------------------------------- /images/gutter/circle-help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/circle-help.png -------------------------------------------------------------------------------- /images/gutter/circle-help@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/circle-help@2x.png -------------------------------------------------------------------------------- /images/gutter/circle-none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/circle-none.png -------------------------------------------------------------------------------- /images/gutter/circle-none@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/circle-none@2x.png -------------------------------------------------------------------------------- /images/gutter/circle-note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/circle-note.png -------------------------------------------------------------------------------- /images/gutter/circle-note@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/circle-note@2x.png -------------------------------------------------------------------------------- /images/gutter/circle-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/circle-warning.png -------------------------------------------------------------------------------- /images/gutter/circle-warning@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/circle-warning@2x.png -------------------------------------------------------------------------------- /images/gutter/shape-error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/shape-error.png -------------------------------------------------------------------------------- /images/gutter/shape-error@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/shape-error@2x.png -------------------------------------------------------------------------------- /images/gutter/shape-help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/shape-help.png -------------------------------------------------------------------------------- /images/gutter/shape-help@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/shape-help@2x.png -------------------------------------------------------------------------------- /images/gutter/shape-none.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/shape-none.png -------------------------------------------------------------------------------- /images/gutter/shape-none@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/shape-none@2x.png -------------------------------------------------------------------------------- /images/gutter/shape-note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/shape-note.png -------------------------------------------------------------------------------- /images/gutter/shape-note@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/shape-note@2x.png -------------------------------------------------------------------------------- /images/gutter/shape-warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/shape-warning.png -------------------------------------------------------------------------------- /images/gutter/shape-warning@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rust-lang/rust-enhanced/b5774b474c13961d2161c8fed20e0c38e6bd97d9/images/gutter/shape-warning@2x.png -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "2.11.0": "changelog/2.11.0.md" 3 | } 4 | -------------------------------------------------------------------------------- /rust/__init__.py: -------------------------------------------------------------------------------- 1 | from . import semver 2 | -------------------------------------------------------------------------------- /rust/batch.py: -------------------------------------------------------------------------------- 1 | """Classes used for aggregating messages that are on the same line.""" 2 | 3 | from . import util 4 | 5 | 6 | class MessageBatch: 7 | 8 | """Abstract base class for a set of messages that apply to the same line. 9 | 10 | :ivar children: List of additional messages, may be empty. 11 | :ivar hidden: Boolean if this message should be displayed. 12 | """ 13 | 14 | hidden = False 15 | 16 | def __init__(self): 17 | self.children = [] 18 | 19 | def __iter__(self): 20 | """Iterates over all messages in the batch.""" 21 | raise NotImplementedError() 22 | 23 | def path(self): 24 | """Returns the file path of the batch.""" 25 | raise NotImplementedError() 26 | 27 | def first(self): 28 | """Returns the first message of the batch.""" 29 | raise NotImplementedError() 30 | 31 | def primary(self): 32 | """Return the primary batch.""" 33 | raise NotImplementedError() 34 | 35 | def dismiss(self, window): 36 | """Permanently remove this message and all its children from the 37 | view.""" 38 | raise NotImplementedError() 39 | 40 | def _dismiss(self, window): 41 | # There is a awkward problem with Sublime and 42 | # add_regions/erase_regions. The regions are part of the undo stack, 43 | # which means even after we erase them, they can come back from the 44 | # dead if the user hits undo. We simply mark these as "hidden" to 45 | # ensure that `clear_messages` can erase any of these zombie regions. 46 | # See https://github.com/SublimeTextIssues/Core/issues/1121 47 | # This is imperfect, since the user could do the following: 48 | # 1) Build 2) Type some text 3) Clear Messages 4) Undo 49 | # which will resurrect the regions without an easy way to remove them 50 | # (user has to close and reopen the file). I don't know of any good 51 | # workarounds. 52 | for msg in self: 53 | views = util.open_views_for_file(window, msg.path) 54 | for view in views: 55 | view.erase_regions(msg.region_key) 56 | view.erase_phantoms(msg.region_key) 57 | 58 | 59 | class PrimaryBatch(MessageBatch): 60 | 61 | """A batch of messages with the primary message. 62 | 63 | :ivar primary_message: The primary message object. 64 | :ivar child_batches: List of `ChildBatch` batches associated with this 65 | batch. 66 | :ivar child_links: List of `(url, text)` tuples for links to child batches 67 | that are "far away". 68 | """ 69 | 70 | primary_message = None 71 | 72 | def __init__(self, primary_message): 73 | super(PrimaryBatch, self).__init__() 74 | self.primary_message = primary_message 75 | self.child_batches = [] 76 | self.child_links = [] 77 | 78 | def __iter__(self): 79 | yield self.primary_message 80 | for child in self.children: 81 | yield child 82 | 83 | def path(self): 84 | return self.primary_message.path 85 | 86 | def first(self): 87 | return self.primary_message 88 | 89 | def primary(self): 90 | return self 91 | 92 | def dismiss(self, window): 93 | self.hidden = True 94 | self._dismiss(window) 95 | for batch in self.child_batches: 96 | batch._dismiss(window) 97 | 98 | 99 | class ChildBatch(MessageBatch): 100 | 101 | """A batch of messages that are associated with a primary message. 102 | 103 | :ivar primary_batch: The `PrimaryBatch` this is associated with. 104 | :ivar back_link: Tuple of `(url, text)` of the link to the primary batch 105 | if it is "far away" (otherwise None). 106 | """ 107 | 108 | primary_batch = None 109 | back_link = None 110 | 111 | def __init__(self, primary_batch): 112 | super(ChildBatch, self).__init__() 113 | self.primary_batch = primary_batch 114 | 115 | def __iter__(self): 116 | for child in self.children: 117 | yield child 118 | 119 | def path(self): 120 | return self.children[0].path 121 | 122 | def first(self): 123 | return self.children[0] 124 | 125 | def primary(self): 126 | return self.primary_batch 127 | 128 | def dismiss(self, window): 129 | self.hidden = True 130 | self._dismiss(window) 131 | -------------------------------------------------------------------------------- /rust/cargo_config.py: -------------------------------------------------------------------------------- 1 | """Sublime commands for configuring Cargo execution. 2 | 3 | See `cargo_settings` for more details on how settings work. 4 | """ 5 | 6 | import getpass 7 | import os 8 | import re 9 | import sublime 10 | import sublime_plugin 11 | from .cargo_settings import CargoSettings, CARGO_COMMANDS 12 | from .util import index_with, get_cargo_metadata 13 | from . import rust_proc, util, log 14 | 15 | # Keep track of recent choices to set the default value. 16 | RECENT_CHOICES = {} 17 | 18 | 19 | class CancelCommandError(Exception): 20 | """Raised when the command should stop.""" 21 | 22 | 23 | class CargoConfigBase(sublime_plugin.WindowCommand): 24 | 25 | """Base class for cargo config commands. 26 | 27 | This implements a simple interactive UI by asking the user a series of 28 | questions using the Sublime quick panels for selecting choices. Subclasses 29 | set the `sequence` class variable to the list of questions they want to 30 | ask. The choices for each question are produced by methods starting with 31 | 'items_'+name. These methods should return a dictionary with: 32 | 33 | - `items`: List of choices. Each element should be a tuple 34 | `(display_string, value)`. 35 | - `default`: The default value (optional). 36 | - `skip_if_one`: Skip this question if there is only 1 item. 37 | - `caption`: Instead of `items`, this is a string displayed with 38 | `show_input_panel` to allow the user to enter arbitrary text. 39 | 40 | `items_` methods can also just return the 'items' list. 41 | 42 | An optional method `selected_`+name will be called when a choice is made. 43 | This method can return a list of questions to be asked immediately. 44 | 45 | The `done` method is called once all questions have been asked. 46 | 47 | Callers are allowed to pass in values instead of using the interactive UI. 48 | This is probably only useful for the test code, but in theory you could 49 | define key bindings that perform certain actions. 50 | """ 51 | 52 | # CargoSettings object. 53 | settings = None 54 | 55 | # Dictionary of choices passed into the command, instead of using 56 | # interactive UI. 57 | cmd_input = None 58 | 59 | # Sequence of questions to ask. 60 | sequence = None 61 | 62 | # Current question being asked. 63 | sequence_index = 0 64 | 65 | # Dictionary of selections made during the interactive process. 66 | choices = None 67 | 68 | # If True, the command wants the 'package' choice to fetch metadata from 69 | # Cargo. 70 | package_wants_metadata = True 71 | 72 | # If True, the 'package' choice will automatically use the manifest 73 | # from the active view if it is available. 74 | package_allows_active_view_shortcut = True 75 | 76 | # If True, 'which' will only allow you choose a package-specific setting. 77 | which_requires_package = False 78 | 79 | # This is a dictionary populated by the `items_package` method. 80 | # Key is the path to a package, the value is the metadata from Cargo. 81 | # This is used by other questions (like `items_target`) to get more 82 | # information about the chosen package. 83 | packages = None 84 | 85 | # Name of what is being configured. 86 | config_name = "" 87 | 88 | def run(self, **kwargs): 89 | self.choices = {} 90 | self.sequence_index = 0 91 | # Copy, since WindowCommand reuses objects. 92 | self._sequence = self.sequence[:] 93 | self.cmd_input = kwargs 94 | self.settings = CargoSettings(self.window) 95 | self.settings.load() 96 | self.show_next_question() 97 | 98 | def done(self): 99 | """Called once all questions have been asked. Subclasses must 100 | implement this.""" 101 | raise NotImplementedError() 102 | 103 | def show_next_question(self): 104 | if self.sequence_index < len(self._sequence): 105 | q = self._sequence[self.sequence_index] 106 | self.sequence_index += 1 107 | else: 108 | self.done() 109 | return 110 | 111 | f_selected = getattr(self, 'selected_' + q, None) 112 | 113 | # Called with the result of what the user selected. 114 | def make_choice(value): 115 | self.choices[q] = value 116 | if f_selected: 117 | try: 118 | next = f_selected(value) 119 | except CancelCommandError: 120 | return 121 | if next: 122 | i = self.sequence_index 123 | self._sequence[i:i] = next 124 | self.show_next_question() 125 | 126 | if q in self.cmd_input: 127 | make_choice(self.cmd_input[q]) 128 | else: 129 | try: 130 | item_info = getattr(self, 'items_' + q)() 131 | except CancelCommandError: 132 | return 133 | if not isinstance(item_info, dict): 134 | item_info = {'items': item_info} 135 | 136 | if 'items' in item_info: 137 | def wrapper(index): 138 | if index != -1: 139 | chosen = item_info['items'][index][1] 140 | RECENT_CHOICES[q] = chosen 141 | make_choice(chosen) 142 | 143 | items = item_info['items'] 144 | if item_info.get('skip_if_one', False) and len(items) == 1: 145 | wrapper(0) 146 | else: 147 | # If the user manually edits the config and enters custom 148 | # values then it won't show up in the list (because it is 149 | # not an exact match). Add it so that it is a valid 150 | # choice (assuming the user entered a valid value). 151 | if 'default' in item_info: 152 | default_index = index_with(items, 153 | lambda x: x[1] == item_info['default']) 154 | if default_index == -1: 155 | items.append((item_info['default'], 156 | item_info['default'])) 157 | # Determine the default selection. 158 | # Use the default provided by the items_ method, else 159 | # use the most recently used value. 160 | default = index_with(items, 161 | lambda x: x[1] == item_info.get('default', 162 | RECENT_CHOICES.get(q, '_NO_DEFAULT_SENTINEL_'))) 163 | display_items = [x[0] for x in items] 164 | self.window.show_quick_panel(display_items, wrapper, 0, 165 | default) 166 | elif 'caption' in item_info: 167 | self.window.show_input_panel(item_info['caption'], 168 | item_info.get('default', ''), 169 | make_choice, None, None) 170 | else: 171 | raise ValueError(item_info) 172 | 173 | def items_package(self): 174 | view = self.window.active_view() 175 | if self.package_allows_active_view_shortcut and view.file_name(): 176 | # If there is a manifest under the current view, use that by 177 | # default. 178 | manifest_dir = util.find_cargo_manifest(view.file_name()) 179 | if manifest_dir: 180 | if self.package_wants_metadata: 181 | metadata = get_cargo_metadata(self.window, manifest_dir) 182 | if metadata: 183 | for package in metadata['packages']: 184 | package_dir = os.path.dirname( 185 | package['manifest_path']) 186 | if package_dir == manifest_dir: 187 | self.packages = { 188 | manifest_dir: package 189 | } 190 | return { 191 | 'items': [(manifest_dir, manifest_dir)], 192 | 'skip_if_one': True, 193 | } 194 | 195 | # Otherwise, hunt for all manifest files and show a list. 196 | folders = self.window.folders() 197 | self.packages = {} 198 | for folder in folders: 199 | folder_parent = os.path.dirname(folder) 200 | for dirpath, dirs, files, in os.walk(folder): 201 | for exclude in ('.git', '.svn'): 202 | if exclude in dirs: 203 | dirs.remove(exclude) 204 | if 'Cargo.toml' in files: 205 | metadata = get_cargo_metadata(self.window, dirpath) 206 | if metadata: 207 | for package in metadata['packages']: 208 | manifest_dir = os.path.dirname(package['manifest_path']) 209 | rel = os.path.relpath(manifest_dir, folder_parent) 210 | package['sublime_relative'] = rel 211 | if manifest_dir not in self.packages: 212 | self.packages[manifest_dir] = package 213 | else: 214 | # Manifest load failure, let it slide. 215 | log.critical(self.window, 216 | 'Failed to load Cargo manifest in %r', dirpath) 217 | 218 | if len(self.packages) == 0: 219 | sublime.error_message(util.multiline_fix(""" 220 | Error: Cannot determine Rust package to use. 221 | 222 | Open a Rust file to determine which package to use, or add a folder with a Cargo.toml file to your Sublime project.""")) 223 | raise CancelCommandError 224 | 225 | def display_name(package): 226 | return ['Package: %s' % (package['name'],), 227 | package['sublime_relative']] 228 | 229 | items = [(display_name(package), path) 230 | for path, package in self.packages.items()] 231 | items.sort(key=lambda x: x[0]) 232 | return { 233 | 'items': items, 234 | 'skip_if_one': True, 235 | } 236 | 237 | def items_target(self): 238 | """Choosing a target requires that 'package' has already been chosen.""" 239 | # Group by kind. 240 | kinds = {} 241 | package_path = self.choices['package'] 242 | for target in self.packages[package_path]['targets']: 243 | # AFAIK, when there are multiple "kind" values, this only happens 244 | # when there are multiple library kinds. 245 | kind = target['kind'][0] 246 | if kind in ('lib', 'rlib', 'dylib', 'cdylib', 'staticlib', 'proc-macro'): 247 | kinds.setdefault('lib', []).append(('Lib', '--lib')) 248 | elif kind in ('bin', 'test', 'example', 'bench'): 249 | text = '%s: %s' % (kind.capitalize(), target['name']) 250 | arg = '--%s %s' % (kind, target['name']) 251 | kinds.setdefault(kind, []).append((text, arg)) 252 | elif kind in ('custom-build',): 253 | # build.rs, can't be built explicitly. 254 | pass 255 | else: 256 | log.critical(self.window, 257 | 'Rust: Unsupported target found: %s', kind) 258 | items = [] 259 | for kind, values in kinds.items(): 260 | allowed = True 261 | if self.choices.get('variant', None): 262 | cmd = CARGO_COMMANDS[self.choices['variant']] 263 | target_types = cmd['allows_target'] 264 | if target_types is not True: 265 | allowed = kind in target_types 266 | if allowed: 267 | items.extend(values) 268 | if not items: 269 | sublime.error_message('Could not determine available targets.') 270 | return items 271 | 272 | def items_variant(self): 273 | result = [] 274 | for key, info in CARGO_COMMANDS.items(): 275 | if self.filter_variant(info): 276 | result.append((info['name'], key)) 277 | result.sort() 278 | return result 279 | 280 | def filter_variant(self, x): 281 | """Subclasses override this to filter variants from the variant 282 | list.""" 283 | return True 284 | 285 | def items_which(self): 286 | """Choice to select at which level the setting should be saved at.""" 287 | # This is a bit of a hack so that when called programmatically you 288 | # don't have to specify 'which'. 289 | if 'which' not in self.cmd_input: 290 | if 'variant' in self.cmd_input: 291 | self.cmd_input['which'] = 'project_package_variant' 292 | elif 'target' in self.cmd_input: 293 | self.cmd_input['which'] = 'project_package_target' 294 | 295 | variant_extra = 'cargo build, cargo run, cargo test, etc.' 296 | target_extra = '--bin, --example, --test, etc.' 297 | result = [] 298 | if not self.which_requires_package: 299 | result.extend([ 300 | (['Set %s globally.', 'Updates RustEnhanced.sublime-settings'], 301 | 'global_default'), 302 | (['Set %s in this Sublime project.', ''], 303 | 'project_default'), 304 | (['Set %s globally for a Build Variant.', variant_extra], 305 | 'global_variant'), 306 | (['Set %s in this Sublime project for a Build Variant (all Cargo packages).', variant_extra], 307 | 'project_variant'), 308 | ]) 309 | result.extend([ 310 | (['Set %s in this Sublime project for all commands (specific Cargo package).', ''], 311 | 'project_package_default'), 312 | (['Set %s in this Sublime project for a Build Variant (specific Cargo package).', variant_extra], 313 | 'project_package_variant'), 314 | (['Set %s in this Sublime project for a Target (specific Cargo package).', target_extra], 315 | 'project_package_target'), 316 | ]) 317 | for (text, _) in result: 318 | text[0] = text[0] % (self.config_name,) 319 | return result 320 | 321 | def selected_which(self, which): 322 | if which in ('project_variant', 'global_variant'): 323 | return ['variant'] 324 | elif which == 'project_package_default': 325 | return ['package'] 326 | elif which == 'project_package_variant': 327 | return ['package', 'variant'] 328 | elif which == 'project_package_target': 329 | return ['package', 'target'] 330 | 331 | def get_setting(self, name, default=None): 332 | """Retrieve a setting, honoring the "which" selection.""" 333 | w = self.choices['which'] 334 | if w == 'global_default': 335 | return self.settings.get_global_default(name, default) 336 | elif w == 'project_default': 337 | return self.settings.get_project_default(name, default) 338 | elif w == 'global_variant': 339 | return self.settings.get_global_variant(self.choices['variant'], 340 | name, default) 341 | elif w == 'project_variant': 342 | return self.settings.get_project_variant(self.choices['variant'], 343 | name, default) 344 | elif w == 'project_package_default': 345 | return self.settings.get_project_package_default( 346 | self.choices['package'], name, default) 347 | elif w == 'project_package_variant': 348 | return self.settings.get_project_package_variant( 349 | self.choices['package'], self.choices['variant'], name, default) 350 | elif w == 'project_package_target': 351 | return self.settings.get_project_package_target( 352 | self.choices['package'], self.choices['target'], name, default) 353 | else: 354 | raise AssertionError(w) 355 | 356 | def set_setting(self, name, value): 357 | """Set a setting, honoring the "which" selection.""" 358 | w = self.choices['which'] 359 | if w == 'global_default': 360 | return self.settings.set_global_default(name, value) 361 | elif w == 'project_default': 362 | return self.settings.set_project_default(name, value) 363 | elif w == 'global_variant': 364 | return self.settings.set_global_variant(self.choices['variant'], 365 | name, value) 366 | elif w == 'project_variant': 367 | return self.settings.set_project_variant(self.choices['variant'], 368 | name, value) 369 | elif w == 'project_package_default': 370 | return self.settings.set_project_package_default( 371 | self.choices['package'], name, value) 372 | elif w == 'project_package_variant': 373 | return self.settings.set_project_package_variant( 374 | self.choices['package'], self.choices['variant'], name, value) 375 | elif w == 'project_package_target': 376 | return self.settings.set_project_package_target( 377 | self.choices['package'], self.choices['target'], name, value) 378 | else: 379 | raise AssertionError(w) 380 | 381 | toolchain_allows_default = True 382 | 383 | def items_toolchain(self): 384 | items = [] 385 | if self.toolchain_allows_default: 386 | items.append(('Use Default Toolchain', None)) 387 | toolchains = self._toolchain_list() 388 | current = self.get_setting('toolchain') 389 | items.extend([(x, x) for x in toolchains]) 390 | result = { 391 | 'items': items, 392 | } 393 | if self.toolchain_allows_default or current: 394 | result['default'] = current 395 | return result 396 | 397 | def _toolchain_list(self): 398 | output = rust_proc.check_output(self.window, 399 | 'rustup toolchain list'.split(), 400 | None) 401 | output = output.splitlines() 402 | system_default = index_with(output, lambda x: x.endswith(' (default)')) 403 | if system_default != -1: 404 | # Strip the " (default)" text. 405 | output[system_default] = output[system_default][:-10] 406 | # Rustup supports some shorthand of either `channel` or `channel-date` 407 | # without the trailing target info. 408 | # 409 | # Complete list of available toolchains is available at: 410 | # https://static.rust-lang.org/dist/index.html 411 | # (See https://github.com/rust-lang-nursery/rustup.rs/issues/215) 412 | shorthands = [] 413 | channels = ['nightly', 'beta', 'stable', '\d\.\d{1,2}\.\d'] 414 | pattern = '(%s)(?:-(\d{4}-\d{2}-\d{2}))?(?:-(.*))' % '|'.join(channels) 415 | for toolchain in output: 416 | m = re.match(pattern, toolchain) 417 | # Should always match. 418 | if m: 419 | channel = m.group(1) 420 | date = m.group(2) 421 | if date: 422 | shorthand = '%s-%s' % (channel, date) 423 | else: 424 | shorthand = channel 425 | if shorthand not in shorthands: 426 | shorthands.append(shorthand) 427 | result = shorthands + output 428 | result.sort() 429 | return result 430 | 431 | 432 | class CargoConfigPackage(CargoConfigBase): 433 | 434 | """This is a fake command used by cargo_build to reuse the code to choose 435 | a Cargo package.""" 436 | 437 | config_name = 'Package' 438 | sequence = ['package'] 439 | package_wants_metadata = False 440 | 441 | def run(self, on_done): 442 | self._on_done = on_done 443 | super(CargoConfigPackage, self).run() 444 | 445 | def done(self): 446 | self._on_done(self.choices['package']) 447 | 448 | 449 | class CargoSetProfile(CargoConfigBase): 450 | 451 | config_name = 'Profile' 452 | sequence = ['which', 'profile'] 453 | 454 | def items_profile(self): 455 | default = self.get_setting('release', False) 456 | if default: 457 | default = 'release' 458 | else: 459 | default = 'dev' 460 | items = [('Dev', 'dev'), 461 | ('Release', 'release')] 462 | return {'items': items, 463 | 'default': default} 464 | 465 | def done(self): 466 | self.set_setting('release', self.choices['profile'] == 'release') 467 | 468 | 469 | class CargoSetTarget(CargoConfigBase): 470 | 471 | config_name = 'Target' 472 | sequence = ['package', 'variant', 'target'] 473 | 474 | def filter_variant(self, info): 475 | return super(CargoSetTarget, self).filter_variant(info) and \ 476 | info.get('allows_target', False) 477 | 478 | def items_target(self): 479 | items = super(CargoSetTarget, self).items_target() 480 | items.insert(0, ('Automatic Detection', 'auto')) 481 | default = self.settings.get_project_package_variant( 482 | self.choices['package'], self.choices['variant'], 'target') 483 | result = { 484 | 'items': items 485 | } 486 | if default: 487 | result['default'] = default 488 | return result 489 | 490 | def done(self): 491 | self.settings.set_project_package_variant(self.choices['package'], 492 | self.choices['variant'], 493 | 'target', 494 | self.choices['target']) 495 | 496 | 497 | class CargoSetTriple(CargoConfigBase): 498 | 499 | config_name = 'Triple' 500 | sequence = ['which', 'toolchain', 'target_triple'] 501 | toolchain_allows_default = False 502 | 503 | def items_target_triple(self): 504 | # Could check if rustup is not installed, to run 505 | # "rustc --print target-list", but that does not tell 506 | # us which targets are installed. 507 | 508 | # The target list depends on the toolchain used. 509 | cmd = 'rustup target list --toolchain=%s' % self.choices['toolchain'] 510 | triples = rust_proc.check_output(self.window, cmd.split(), None)\ 511 | .splitlines() 512 | current = self.get_setting('target_triple') 513 | result = [('Use Default', None)] 514 | for triple in triples: 515 | if triple.endswith(' (default)'): 516 | actual_triple = triple[:-10] 517 | result.append((actual_triple, actual_triple)) 518 | elif triple.endswith(' (installed)'): 519 | actual_triple = triple[:-12] 520 | result.append((actual_triple, actual_triple)) 521 | else: 522 | actual_triple = None 523 | # Don't bother listing uninstalled targets. 524 | return { 525 | 'items': result, 526 | 'default': current 527 | } 528 | 529 | def done(self): 530 | self.set_setting('target_triple', self.choices['target_triple']) 531 | 532 | 533 | class CargoSetToolchain(CargoConfigBase): 534 | 535 | config_name = 'Toolchain' 536 | sequence = ['which', 'toolchain'] 537 | 538 | def done(self): 539 | self.set_setting('toolchain', self.choices['toolchain']) 540 | 541 | 542 | class CargoSetFeatures(CargoConfigBase): 543 | 544 | config_name = 'Features' 545 | sequence = ['which', 'no_default_features', 'features'] 546 | which_requires_package = True 547 | 548 | def items_no_default_features(self): 549 | current = self.get_setting('no_default_features', False) 550 | items = [ 551 | ('Include default features.', False), 552 | ('Do not include default features.', True) 553 | ] 554 | return { 555 | 'items': items, 556 | 'default': current, 557 | } 558 | 559 | def items_features(self): 560 | features = self.get_setting('features', None) 561 | if features is None: 562 | # Detect available features from the manifest. 563 | package_path = self.choices['package'] 564 | available_features = self.packages[package_path].get('features', {}) 565 | items = list(available_features.keys()) 566 | # Remove the "default" entry. 567 | if 'default' in items: 568 | del items[items.index('default')] 569 | if not self.choices['no_default_features']: 570 | # Don't show default features, (they are already included). 571 | for ft in available_features['default']: 572 | if ft in items: 573 | del items[items.index(ft)] 574 | features = ' '.join(items) 575 | return { 576 | 'caption': 'Choose features (space separated, use "ALL" to use all features)', 577 | 'default': features, 578 | } 579 | 580 | def done(self): 581 | self.set_setting('no_default_features', 582 | self.choices['no_default_features']) 583 | self.set_setting('features', self.choices['features']) 584 | 585 | 586 | class CargoSetDefaultPath(CargoConfigBase): 587 | 588 | config_name = 'Default Path' 589 | sequence = ['package'] 590 | package_allows_active_view_shortcut = False 591 | 592 | def items_package(self): 593 | result = super(CargoSetDefaultPath, self).items_package() 594 | items = result['items'] 595 | items.insert(0, (['No Default', 596 | 'Build will attempt to detect from the current view, or pop up a selection panel.'], 597 | None)) 598 | result['default'] = self.settings.get_project_base('default_path') 599 | return result 600 | 601 | def done(self): 602 | self.settings.set_project_base('default_path', self.choices['package']) 603 | 604 | 605 | class CargoSetEnvironmentEditor(CargoConfigBase): 606 | 607 | config_name = 'Environment' 608 | sequence = ['which'] 609 | 610 | def done(self): 611 | view = self.window.new_file() 612 | view.set_scratch(True) 613 | default = self.get_setting('env') 614 | template = util.multiline_fix(""" 615 | // Enter environment variables here in JSON syntax. 616 | // Close this view when done to commit the settings. 617 | """) 618 | if 'contents' in self.cmd_input: 619 | # Used when parsing fails to attempt to edit again. 620 | template = self.cmd_input['contents'] 621 | elif default: 622 | template += sublime.encode_value(default, True) 623 | else: 624 | template += util.multiline_fix(""" 625 | { 626 | // "RUST_BACKTRACE": "1" 627 | } 628 | """) 629 | # Unfortunately Sublime indents on 'insert' 630 | view.settings().set('auto_indent', False) 631 | view.run_command('insert', {'characters': template}) 632 | view.settings().set('auto_indent', True) 633 | view.set_syntax_file('Packages/JavaScript/JSON.sublime-syntax') 634 | view.settings().set('rust_environment_editor', True) 635 | view.settings().set('rust_environment_editor_settings', { 636 | 'package': self.choices.get('package'), 637 | 'which': self.choices['which'], 638 | 'variant': self.choices.get('variant'), 639 | 'target': self.choices.get('target'), 640 | }) 641 | 642 | 643 | class CargoSetEnvironment(CargoConfigBase): 644 | 645 | """Special command that should not be run interactively. Used by the 646 | on-close callback to actually set the environment.""" 647 | 648 | config_name = 'Environment' 649 | sequence = ['which', 'env'] 650 | 651 | def items_env(self): 652 | return [] 653 | 654 | def done(self): 655 | self.set_setting('env', self.choices['env']) 656 | 657 | 658 | class EnvironmentSaveHandler(sublime_plugin.EventListener): 659 | 660 | """Handler for when the view is closed on the environment editor.""" 661 | 662 | def on_pre_close(self, view): 663 | if not view.settings().get('rust_environment_editor'): 664 | return 665 | settings = view.settings().get('rust_environment_editor_settings') 666 | 667 | contents = view.substr(sublime.Region(0, view.size())) 668 | try: 669 | result = sublime.decode_value(contents) 670 | except: 671 | sublime.error_message('Value was not valid JSON, try again.') 672 | view.window().run_command('cargo_set_environment_editor', { 673 | 'package': settings.get('package'), 674 | 'which': settings['which'], 675 | 'variant': settings.get('variant'), 676 | 'target': settings.get('target'), 677 | 'contents': contents, 678 | }) 679 | return 680 | 681 | view.window().run_command('cargo_set_environment', { 682 | 'package': settings.get('package'), 683 | 'which': settings['which'], 684 | 'variant': settings.get('variant'), 685 | 'target': settings.get('target'), 686 | 'env': result, 687 | }) 688 | 689 | 690 | class CargoSetArguments(CargoConfigBase): 691 | 692 | config_name = 'Extra Command-line Arguments' 693 | sequence = ['which', 'before_after', 'args'] 694 | 695 | def items_before_after(self): 696 | return [ 697 | ('Enter extra Cargo arguments (before -- separator)', 'extra_cargo_args'), 698 | ('Enter extra Cargo arguments (after -- separator)', 'extra_run_args'), 699 | ] 700 | 701 | def items_args(self): 702 | current = self.get_setting(self.choices['before_after'], '') 703 | return { 704 | 'caption': 'Enter the extra Cargo args', 705 | 'default': current, 706 | } 707 | 708 | def done(self): 709 | self.set_setting(self.choices['before_after'], 710 | self.choices['args']) 711 | 712 | 713 | class CargoConfigure(CargoConfigBase): 714 | 715 | sequence = ['config_option'] 716 | 717 | def items_config_option(self): 718 | return [ 719 | (['Set Target', '--bin, --lib, --example, etc.'], 'target'), 720 | (['Set Profile', '--release flag'], 'profile'), 721 | (['Set Target Triple', '--target flag'], 'triple'), 722 | (['Set Rust Toolchain', 'nightly vs stable, etc.'], 'toolchain'), 723 | (['Set Features', 'Cargo build features with --features'], 'features'), 724 | (['Set Environment Variables', ''], 'environment'), 725 | (['Set Extra Cargo Arguments', ''], 'args'), 726 | (['Set Default Package/Path', ''], 'package'), 727 | ] 728 | 729 | def selected_config_option(self, which): 730 | if which == 'target': 731 | CargoSetTarget(self.window).run() 732 | elif which == 'profile': 733 | CargoSetProfile(self.window).run() 734 | elif which == 'triple': 735 | CargoSetTriple(self.window).run() 736 | elif which == 'toolchain': 737 | CargoSetToolchain(self.window).run() 738 | elif which == 'features': 739 | CargoSetFeatures(self.window).run() 740 | elif which == 'environment': 741 | CargoSetEnvironmentEditor(self.window).run() 742 | elif which == 'args': 743 | CargoSetArguments(self.window).run() 744 | elif which == 'package': 745 | CargoSetDefaultPath(self.window).run() 746 | else: 747 | raise AssertionError(which) 748 | 749 | def done(self): 750 | pass 751 | 752 | 753 | class CargoCreateNewBuild(CargoConfigBase): 754 | 755 | """Command to create a new build variant, stored in the user's 756 | `.sublime-project` file.""" 757 | 758 | config_name = 'New Build' 759 | sequence = ['command'] 760 | 761 | def items_command(self): 762 | if self.window.project_data() is None: 763 | sublime.error_message(util.multiline_fix(""" 764 | Error: This command requires a .sublime-project file. 765 | 766 | Save your Sublime project and try again.""")) 767 | raise CancelCommandError 768 | result = [] 769 | for key, info in CARGO_COMMANDS.items(): 770 | if self.filter_variant(info): 771 | result.append((info['name'], key)) 772 | result.sort() 773 | result.append(('New Command', 'NEW_COMMAND')) 774 | return result 775 | 776 | def selected_command(self, command): 777 | if command == 'NEW_COMMAND': 778 | return ['new_command', 'allows_target', 'allows_target_triple', 779 | 'allows_release', 'allows_features', 'allows_json', 780 | 'requires_manifest', 'requires_view_path', 'wants_run_args', 781 | 'name'] 782 | else: 783 | cinfo = CARGO_COMMANDS[command] 784 | result = [] 785 | if cinfo.get('requires_manifest', True): 786 | result.append('package') 787 | result.append('name') 788 | return result 789 | 790 | def items_package(self): 791 | result = super(CargoCreateNewBuild, self).items_package() 792 | if len(result['items']) > 1: 793 | result['items'].insert(0, (['Any Package', 794 | 'This build variant is not tied to any particular Cargo package.'], 795 | None)) 796 | return result 797 | 798 | def selected_package(self, package): 799 | if package: 800 | cinfo = CARGO_COMMANDS[self.choices['command']] 801 | if cinfo.get('allows_target', False): 802 | return ['target'] 803 | 804 | def items_new_command(self): 805 | return { 806 | 'caption': 'Enter the Cargo subcommand to run:', 807 | } 808 | 809 | def selected_new_command(self, command): 810 | if not command: 811 | sublime.error_message('Error: You must enter a command to run.') 812 | raise CancelCommandError 813 | 814 | def items_allows_target(self): 815 | return [ 816 | ('Command %r supports Cargo filters (--bin, --example, etc.)' % ( 817 | self.choices['new_command']), True), 818 | ('Command %r does not support target filters' % ( 819 | self.choices['new_command'],), False) 820 | ] 821 | 822 | def items_allows_target_triple(self): 823 | return [ 824 | ('Command %r supports --target triple flag' % ( 825 | self.choices['new_command']), True), 826 | ('Command %r does not support --target' % ( 827 | self.choices['new_command'],), False) 828 | ] 829 | 830 | def items_allows_release(self): 831 | return [ 832 | ('Command %r supports --release flag' % ( 833 | self.choices['new_command']), True), 834 | ('Command %r does not support --release' % ( 835 | self.choices['new_command'],), False) 836 | ] 837 | 838 | def items_allows_features(self): 839 | return [ 840 | ('Command %r supports --features flag' % ( 841 | self.choices['new_command']), True), 842 | ('Command %r does not support --features' % ( 843 | self.choices['new_command'],), False) 844 | ] 845 | 846 | def items_allows_json(self): 847 | return [ 848 | ('Command %r supports --message-format=json flag' % ( 849 | self.choices['new_command']), True), 850 | ('Command %r does not support JSON' % ( 851 | self.choices['new_command'],), False) 852 | ] 853 | 854 | def items_requires_manifest(self): 855 | return [ 856 | ('Command %r requires a Cargo.toml manifest' % ( 857 | self.choices['new_command']), True), 858 | ('Command %r does not require a manifest' % ( 859 | self.choices['new_command'],), False) 860 | ] 861 | 862 | def items_requires_view_path(self): 863 | return [ 864 | ('Do not include view path', False), 865 | ('Include path of active sublime view on command line', True), 866 | ] 867 | 868 | def items_wants_run_args(self): 869 | return [ 870 | ('Do not ask for more arguments', False), 871 | ('Ask for extra command-line arguments each time', True), 872 | ] 873 | 874 | def items_name(self): 875 | name = '%s\'s %s' % (getpass.getuser(), 876 | self.choices.get('new_command', self.choices['command'])) 877 | target = self.choices.get('target', None) 878 | if target: 879 | target = target.replace('-', '') 880 | name = name + ' %s' % (target,) 881 | return { 882 | 'caption': 'Enter a name for your new Cargo build system:', 883 | 'default': name 884 | } 885 | 886 | def selected_name(self, name): 887 | if not name: 888 | sublime.error_message('Error: You must enter a name.') 889 | raise CancelCommandError 890 | 891 | def done(self): 892 | proj_data = self.window.project_data() 893 | systems = proj_data.setdefault('build_systems', []) 894 | for system_index, system in enumerate(systems): 895 | if system.get('target') == 'cargo_exec': 896 | break 897 | else: 898 | system = self._stock_build_system() 899 | system['name'] = 'Custom Cargo Build' 900 | system_index = len(systems) 901 | systems.append(system) 902 | variants = system.setdefault('variants', []) 903 | 904 | # Add the defaults to make it easier to manually edit. 905 | settings = { 906 | 'release': False, 907 | 'target_triple': '', 908 | 'toolchain': '', 909 | 'target': '', 910 | 'no_default_features': False, 911 | 'features': '', 912 | 'extra_cargo_args': '', 913 | 'extra_run_args': '', 914 | 'env': {}, 915 | } 916 | cinfo = {} 917 | result = { 918 | 'name': self.choices['name'], 919 | 'target': 'cargo_exec', 920 | 'command': self.choices.get('new_command', 921 | self.choices['command']), 922 | 'settings': settings, 923 | 'command_info': cinfo, 924 | } 925 | if self.choices['command'] == 'NEW_COMMAND': 926 | for key in ['allows_target', 'allows_target_triple', 927 | 'allows_release', 'allows_features', 'allows_json', 928 | 'requires_manifest', 'requires_view_path', 929 | 'wants_run_args']: 930 | cinfo[key] = self.choices[key] 931 | requires_view_path = cinfo.get('requires_view_path') 932 | else: 933 | if 'target' in self.choices: 934 | settings['target'] = self.choices['target'] 935 | if 'package' in self.choices: 936 | settings['working_dir'] = self.choices['package'] 937 | requires_view_path = CARGO_COMMANDS[self.choices['command']]\ 938 | .get('requires_view_path', False) 939 | 940 | if requires_view_path and util.active_view_is_rust(): 941 | settings['script_path'] = self.window.active_view().file_name() 942 | 943 | variants.insert(0, result) 944 | self.window.set_project_data(proj_data) 945 | self.window.run_command('set_build_system', {'index': system_index}) 946 | 947 | def _stock_build_system(self): 948 | pkg_name = __name__.split('.')[0] 949 | resource = 'Packages/%s/RustEnhanced.sublime-build' % pkg_name 950 | return sublime.decode_value(sublime.load_resource(resource)) 951 | -------------------------------------------------------------------------------- /rust/cargo_settings.py: -------------------------------------------------------------------------------- 1 | """Interface for accessing Cargo settings (stored in the sublime-project 2 | file). 3 | 4 | These are used by the build system to determine how to run Cargo. 5 | 6 | Cargo Info 7 | ========== 8 | When the `cargo_exec` Sublime command is run, you pass in a named command to 9 | run. There is a default set of commands defined here in CARGO_COMMANDS (users 10 | can create custom commands and pass them in with `command_info`). See 11 | `docs/build.md` for a description of the different `command_info` values. 12 | 13 | Project Settings 14 | ================ 15 | Settings can be stored (under the "cargo_build" key) to alter how cargo is 16 | run. See `docs/build.md` for a description. 17 | """ 18 | 19 | import sublime 20 | import os 21 | import shlex 22 | from . import util, target_detect, log 23 | 24 | CARGO_COMMANDS = { 25 | 'auto': { 26 | 'name': 'Automatic', 27 | 'command': 'auto', 28 | 'allows_target': True, 29 | 'allows_target_triple': True, 30 | 'allows_release': True, 31 | 'allows_features': True, 32 | 'allows_json': True, 33 | }, 34 | 'build': { 35 | 'name': 'Build', 36 | 'command': 'build', 37 | 'allows_target': True, 38 | 'allows_target_triple': True, 39 | 'allows_release': True, 40 | 'allows_features': True, 41 | 'allows_json': True, 42 | }, 43 | 'run': { 44 | 'name': 'Run', 45 | 'command': 'run', 46 | 'allows_target': ('bin', 'example'), 47 | 'allows_target_triple': True, 48 | 'allows_release': True, 49 | 'allows_features': True, 50 | 'allows_json': True, 51 | 'json_stop_pattern': '^\s*Running ', 52 | }, 53 | 'check': { 54 | 'name': 'Check', 55 | 'command': 'check', 56 | 'allows_target': True, 57 | 'allows_target_triple': True, 58 | 'allows_release': True, 59 | 'allows_features': True, 60 | 'allows_json': True, 61 | }, 62 | 'test': { 63 | 'name': 'Test', 64 | 'command': 'test', 65 | 'allows_target': True, 66 | 'allows_target_triple': True, 67 | 'allows_release': True, 68 | 'allows_features': True, 69 | 'allows_json': True, 70 | }, 71 | 'bench': { 72 | 'name': 'Bench', 73 | 'command': 'bench', 74 | 'allows_target': True, 75 | 'allows_target_triple': True, 76 | 'allows_release': False, 77 | 'allows_features': True, 78 | 'allows_json': True, 79 | }, 80 | 'clean': { 81 | 'name': 'Clean', 82 | 'command': 'clean', 83 | }, 84 | 'doc': { 85 | 'name': 'Doc', 86 | 'command': 'doc', 87 | 'allows_target': ['lib', 'bin'], 88 | 'allows_target_triple': True, 89 | 'allows_release': True, 90 | 'allows_features': True, 91 | 'allows_json': True, 92 | }, 93 | 'clippy': { 94 | 'name': 'Clippy', 95 | 'command': 'clippy', 96 | 'allows_target': True, 97 | 'allows_target_triple': True, 98 | 'allows_release': True, 99 | 'allows_features': True, 100 | 'allows_json': True, 101 | }, 102 | } 103 | 104 | 105 | CARGO_BUILD_DEFAULTS = { 106 | } 107 | 108 | 109 | class CargoSettings(object): 110 | 111 | """Interface to Cargo project settings stored in `sublime-project` 112 | file.""" 113 | 114 | # Sublime window. 115 | window = None 116 | # Data in the sublime project file. Empty dictionary if nothing is set. 117 | project_data = None 118 | 119 | def __init__(self, window): 120 | self.window = window 121 | 122 | def load(self): 123 | self.project_data = self.window.project_data() 124 | if self.project_data is None: 125 | # Window does not have a Sublime project. 126 | self.project_data = {} 127 | self.re_settings = sublime.load_settings('RustEnhanced.sublime-settings') 128 | 129 | def get_global_default(self, key, default=None): 130 | internal_default = CARGO_BUILD_DEFAULTS.get('defaults', {})\ 131 | .get(key, default) 132 | return self.re_settings.get('cargo_build', {})\ 133 | .get('defaults', {})\ 134 | .get(key, internal_default) 135 | 136 | def set_global_default(self, key, value): 137 | cb = self.re_settings.get('cargo_build', {}) 138 | cb.setdefault('defaults', {})[key] = value 139 | self.re_settings.set('cargo_build', cb) 140 | sublime.save_settings('RustEnhanced.sublime-settings') 141 | 142 | def get_project_default(self, key, default=None): 143 | return self.project_data.get('settings', {})\ 144 | .get('cargo_build', {})\ 145 | .get('defaults', {})\ 146 | .get(key, default) 147 | 148 | def set_project_default(self, key, value): 149 | self.project_data.setdefault('settings', {})\ 150 | .setdefault('cargo_build', {})\ 151 | .setdefault('defaults', {})[key] = value 152 | self._set_project_data() 153 | 154 | def get_global_variant(self, variant, key, default=None): 155 | internal_default = CARGO_BUILD_DEFAULTS.get('variants', {})\ 156 | .get(variant, {})\ 157 | .get(key, default) 158 | return self.re_settings.get('cargo_build', {})\ 159 | .get('variants', {})\ 160 | .get(variant, {})\ 161 | .get(key, internal_default) 162 | 163 | def set_global_variant(self, variant, key, value): 164 | cb = self.re_settings.get('cargo_build', {}) 165 | cb.setdefault('variants', {})\ 166 | .setdefault(variant, {})[key] = value 167 | self.re_settings.set('cargo_build', cb) 168 | sublime.save_settings('RustEnhanced.sublime-settings') 169 | 170 | def get_project_variant(self, variant, key, default=None): 171 | return self.project_data.get('settings', {})\ 172 | .get('cargo_build', {})\ 173 | .get('variants', {})\ 174 | .get(variant, {})\ 175 | .get(key, default) 176 | 177 | def set_project_variant(self, variant, key, value): 178 | self.project_data.setdefault('settings', {})\ 179 | .setdefault('cargo_build', {})\ 180 | .setdefault('variants', {})\ 181 | .setdefault(variant, {})[key] = value 182 | self._set_project_data() 183 | 184 | def get_project_package_default(self, path, key, default=None): 185 | path = os.path.normpath(path) 186 | return self.project_data.get('settings', {})\ 187 | .get('cargo_build', {})\ 188 | .get('paths', {})\ 189 | .get(path, {})\ 190 | .get('defaults', {})\ 191 | .get(key, default) 192 | 193 | def set_project_package_default(self, path, key, value): 194 | path = os.path.normpath(path) 195 | self.project_data.setdefault('settings', {})\ 196 | .setdefault('cargo_build', {})\ 197 | .setdefault('paths', {})\ 198 | .setdefault(path, {})\ 199 | .setdefault('defaults', {})[key] = value 200 | self._set_project_data() 201 | 202 | def get_project_package_variant(self, path, variant, key, default=None): 203 | path = os.path.normpath(path) 204 | return self.project_data.get('settings', {})\ 205 | .get('cargo_build', {})\ 206 | .get('paths', {})\ 207 | .get(path, {})\ 208 | .get('variants', {})\ 209 | .get(variant, {})\ 210 | .get(key, default) 211 | 212 | def set_project_package_variant(self, path, variant, key, value): 213 | path = os.path.normpath(path) 214 | self.project_data.setdefault('settings', {})\ 215 | .setdefault('cargo_build', {})\ 216 | .setdefault('paths', {})\ 217 | .setdefault(path, {})\ 218 | .setdefault('variants', {})\ 219 | .setdefault(variant, {})[key] = value 220 | self._set_project_data() 221 | 222 | def get_project_package_target(self, path, target, key, default=None): 223 | path = os.path.normpath(path) 224 | return self.project_data.get('settings', {})\ 225 | .get('cargo_build', {})\ 226 | .get('paths', {})\ 227 | .get(path, {})\ 228 | .get('targets', {})\ 229 | .get(target, {})\ 230 | .get(key, default) 231 | 232 | def set_project_package_target(self, path, target, key, value): 233 | path = os.path.normpath(path) 234 | self.project_data.setdefault('settings', {})\ 235 | .setdefault('cargo_build', {})\ 236 | .setdefault('paths', {})\ 237 | .setdefault(path, {})\ 238 | .setdefault('targets', {})\ 239 | .setdefault(target, {})[key] = value 240 | self._set_project_data() 241 | 242 | def get_project_base(self, key, default=None): 243 | return self.project_data.get('settings', {})\ 244 | .get('cargo_build', {})\ 245 | .get(key, default) 246 | 247 | def set_project_base(self, key, value): 248 | self.project_data.setdefault('settings', {})\ 249 | .setdefault('cargo_build', {})[key] = value 250 | self._set_project_data() 251 | 252 | def _set_project_data(self): 253 | if self.window.project_file_name() is None: 254 | # XXX: Better way to display a warning? Is 255 | # sublime.error_message() reasonable? 256 | log.critical(self.window, util.multiline_fix(""" 257 | Rust Enhanced Warning: This window does not have an associated sublime-project file. 258 | Any changes to the Cargo build settings will be lost if you close the window.""")) 259 | self.window.set_project_data(self.project_data) 260 | 261 | def determine_target(self, cmd_name, settings_path, 262 | cmd_info=None, override=None): 263 | if cmd_info is None: 264 | cmd_info = CARGO_COMMANDS[cmd_name] 265 | 266 | target = None 267 | if cmd_info.get('allows_target', False): 268 | if override: 269 | tcfg = override 270 | else: 271 | tcfg = self.get_project_package_variant(settings_path, cmd_name, 'target') 272 | if tcfg == 'auto': 273 | # If this fails, leave target as None and let Cargo sort it 274 | # out (it may display an error). 275 | if util.active_view_is_rust(): 276 | td = target_detect.TargetDetector(self.window) 277 | view = self.window.active_view() 278 | targets = td.determine_targets(view.file_name()) 279 | if len(targets) == 1: 280 | src_path, cmd_line = targets[0] 281 | target = ' '.join(cmd_line) 282 | else: 283 | target = tcfg 284 | return target 285 | 286 | def get_computed(self, settings_path, variant, target, key, 287 | default=None, initial_settings={}): 288 | """Get the configuration value for the given key.""" 289 | v = initial_settings.get(key) 290 | if v is None: 291 | v = self.get_project_package_target(settings_path, target, key) 292 | if v is None: 293 | v = self.get_project_package_variant(settings_path, variant, key) 294 | if v is None: 295 | v = self.get_project_package_default(settings_path, key) 296 | if v is None: 297 | v = self.get_project_variant(variant, key) 298 | if v is None: 299 | v = self.get_global_variant(variant, key) 300 | if v is None: 301 | v = self.get_project_default(key) 302 | if v is None: 303 | v = self.get_global_default(key, default) 304 | return v 305 | 306 | def get_merged(self, settings_path, variant, target, key, 307 | initial_settings={}): 308 | """Get the configuration value for the given key. 309 | 310 | This assumes the value is a dictionary, and will merge all values from 311 | each level. This is primarily used for the `env` environment 312 | variables. 313 | """ 314 | result = self.get_global_default(key, {}).copy() 315 | 316 | proj_def = self.get_project_default(key, {}) 317 | result.update(proj_def) 318 | 319 | glbl_var = self.get_global_variant(variant, key, {}) 320 | result.update(glbl_var) 321 | 322 | proj_var = self.get_project_variant(variant, key, {}) 323 | result.update(proj_var) 324 | 325 | pp_def = self.get_project_package_default(settings_path, key, {}) 326 | result.update(pp_def) 327 | 328 | pp_var = self.get_project_package_variant(settings_path, variant, key, {}) 329 | result.update(pp_var) 330 | 331 | pp_tar = self.get_project_package_target(settings_path, target, key, {}) 332 | result.update(pp_tar) 333 | 334 | initial = initial_settings.get(key, {}) 335 | result.update(initial) 336 | return result 337 | 338 | def get_command(self, cmd_name, cmd_info, 339 | settings_path, working_dir, 340 | initial_settings={}, force_json=False, 341 | metadata=None): 342 | """Generates the command arguments for running Cargo. 343 | 344 | :param cmd_name: The name of the command, the key used to select a 345 | "variant". 346 | :param cmd_info: Dictionary from `CARGO_COMMANDS` with rules on how to 347 | construct the command. 348 | :param settings_path: The absolute path to the Cargo project root 349 | directory. 350 | :param working_dir: The directory where Cargo is to be run (typically 351 | the project root). 352 | :keyword initial_settings: Initial settings to inject which override 353 | all other settings. 354 | :keyword force_json: If True, will force JSON output. 355 | :keyword metadata: Output from `get_cargo_metadata`. If None, will run 356 | it manually. 357 | 358 | :Returns: A dictionary with the keys: 359 | - `command`: The command to run as a list of strings. 360 | - `env`: Dictionary of environment variables (or None). 361 | - `msg_rel_path`: The root path to use for relative paths in 362 | messages. 363 | - `rustc_version`: The version of rustc being used as a string, 364 | such as '1.25.0-nightly'. 365 | 366 | Returns None if the command cannot be constructed. 367 | """ 368 | target = self.determine_target(cmd_name, settings_path, 369 | cmd_info=cmd_info, override=initial_settings.get('target')) 370 | 371 | def get_computed(key, default=None): 372 | return self.get_computed(settings_path, cmd_name, target, key, 373 | default=default, initial_settings=initial_settings) 374 | 375 | result = ['cargo'] 376 | 377 | toolchain = get_computed('toolchain', None) 378 | if toolchain: 379 | result.append('+' + toolchain) 380 | 381 | # Command to run. 382 | result.append(cmd_info['command']) 383 | 384 | # Default target. 385 | if target: 386 | result.extend(target.split()) 387 | 388 | # target_triple 389 | if cmd_info.get('allows_target_triple', False): 390 | v = get_computed('target_triple', None) 391 | if v: 392 | result.extend(['--target', v]) 393 | 394 | # release (profile) 395 | if cmd_info.get('allows_release', False): 396 | v = get_computed('release', False) 397 | if v: 398 | result.append('--release') 399 | 400 | if force_json or (cmd_info.get('allows_json', False) and 401 | util.get_setting('show_errors_inline', True)): 402 | result.append('--message-format=json') 403 | 404 | # features 405 | if cmd_info.get('allows_features', False): 406 | v = get_computed('no_default_features', False) 407 | if v: 408 | result.append('--no-default-features') 409 | v = get_computed('features', None) 410 | if v: 411 | if v.upper() == 'ALL': 412 | result.append('--all-features') 413 | else: 414 | result.append('--features') 415 | result.append(v) 416 | 417 | # Add path from current active view (mainly for "cargo script", now unused). 418 | if cmd_info.get('requires_view_path', False): 419 | script_path = get_computed('script_path') 420 | if not script_path: 421 | if not util.active_view_is_rust(): 422 | sublime.error_message(util.multiline_fix(""" 423 | Cargo build command %r requires the current view to be a Rust source file.""" % cmd_info['name'])) 424 | return None 425 | script_path = self.window.active_view().file_name() 426 | result.append(script_path) 427 | 428 | def expand(s): 429 | return sublime.expand_variables(s, 430 | self.window.extract_variables()) 431 | 432 | # Extra args. 433 | extra_cargo_args = get_computed('extra_cargo_args') 434 | if extra_cargo_args: 435 | extra_cargo_args = expand(extra_cargo_args) 436 | result.extend(shlex.split(extra_cargo_args)) 437 | 438 | extra_run_args = get_computed('extra_run_args') 439 | if extra_run_args: 440 | extra_run_args = expand(extra_run_args) 441 | result.append('--') 442 | result.extend(shlex.split(extra_run_args)) 443 | 444 | # Compute the environment. 445 | env = self.get_merged(settings_path, cmd_name, target, 'env', 446 | initial_settings=initial_settings) 447 | for k, v in env.items(): 448 | env[k] = os.path.expandvars(v) 449 | if not env: 450 | env = None 451 | 452 | # Determine the base path for paths in messages. 453 | # 454 | # Starting in Rust 1.24, all messages and symbols are relative to the 455 | # workspace root instead of the package root. 456 | if metadata is None: 457 | metadata = util.get_cargo_metadata(self.window, working_dir, toolchain) 458 | if metadata and 'workspace_root' in metadata: 459 | # 'workspace_root' key added in 1.24. 460 | msg_rel_path = metadata['workspace_root'] 461 | else: 462 | msg_rel_path = working_dir 463 | 464 | rustc_version = util.get_rustc_version(self.window, working_dir, toolchain=toolchain) 465 | 466 | return { 467 | 'command': result, 468 | 'env': env, 469 | 'msg_rel_path': msg_rel_path, 470 | 'rustc_version': rustc_version, 471 | } 472 | -------------------------------------------------------------------------------- /rust/levels.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | from . import log 3 | 4 | 5 | class Level: 6 | 7 | def __init__(self, order, name, plural): 8 | self.order = order 9 | self.name = name 10 | self.plural = plural 11 | 12 | def __hash__(self): 13 | return hash(self.name) 14 | 15 | def __eq__(self, other): 16 | if isinstance(other, Level): 17 | return self.name == other.name 18 | elif isinstance(other, str): 19 | return self.name == other 20 | else: 21 | return False 22 | 23 | def __lt__(self, other): 24 | return self.order < other.order 25 | 26 | def __le__(self, other): 27 | return self.order <= other.order 28 | 29 | def __gt__(self, other): 30 | return self.order > other.order 31 | 32 | def __ge__(self, other): 33 | return self.order >= other.order 34 | 35 | def __repr__(self): 36 | return self.name 37 | 38 | 39 | LEVELS = { 40 | 'error': Level(0, 'error', 'errors'), 41 | 'warning': Level(1, 'warning', 'warnings'), 42 | 'note': Level(2, 'note', 'notes'), 43 | 'help': Level(3, 'help', 'help'), 44 | # This is "FailureNote", see https://github.com/rust-lang/rust/issues/60425. 45 | # Currently we filter all these out ("For more information..."), but 46 | # handle it just in case new ones are added. 47 | '': Level(4, 'note', 'note'), 48 | } 49 | 50 | 51 | def level_from_str(level): 52 | if level.startswith('error:'): 53 | # ICE 54 | level = 'error' 55 | try: 56 | return LEVELS[level] 57 | except KeyError: 58 | log.critical(sublime.active_window(), 59 | 'RustEnhanced: Unknown message level %r encountered.', 60 | level) 61 | return LEVELS['error'] 62 | -------------------------------------------------------------------------------- /rust/log.py: -------------------------------------------------------------------------------- 1 | """Debug logging support.""" 2 | 3 | import sublime_plugin 4 | import time 5 | 6 | 7 | logs = {} 8 | 9 | 10 | class WindowLog: 11 | 12 | """Collection of log messages tied to a window.""" 13 | 14 | view = None 15 | 16 | def __init__(self): 17 | self.messages = [] 18 | 19 | def clear(self): 20 | self.messages.clear() 21 | if self.view: 22 | self.view.run_command("select_all") 23 | self.view.run_command("right_delete") 24 | 25 | def add_message(self, msg, args): 26 | if self.messages: 27 | previous_time = self.messages[-1].time 28 | else: 29 | previous_time = None 30 | lm = LogMessage(msg, args, previous_time) 31 | self.messages.append(lm) 32 | self._display_message(lm) 33 | 34 | def _display_message(self, msg): 35 | if self.view: 36 | text = msg.render() 37 | self.view.run_command('append', {'characters': text, 38 | 'scroll_to_end': True}) 39 | 40 | def open_view(self, window): 41 | view = window.new_file() 42 | view.set_scratch(True) 43 | view.settings().set('rust_log_view', window.id()) 44 | view.settings().set('word_wrap', True) 45 | view.set_name('Rust Enhanced Debug Log') 46 | self.view = view 47 | for m in self.messages: 48 | self._display_message(m) 49 | 50 | 51 | class LogMessage: 52 | def __init__(self, msg, args, previous_time): 53 | self.msg = msg 54 | self.args = args 55 | self.previous_time = previous_time 56 | self.time = time.time() 57 | 58 | def render(self): 59 | if self.previous_time is None: 60 | last_time = '+0.000' 61 | else: 62 | last_time = '+%.3f' % (self.time - self.previous_time,) 63 | if self.args: 64 | rendered = self.msg % self.args 65 | else: 66 | rendered = self.msg 67 | return '%s %s\n' % (last_time, rendered.rstrip()) 68 | 69 | 70 | def critical(window, msg, *args): 71 | """Add a log message and display it to the console.""" 72 | log(window, msg, *args) 73 | if args: 74 | print(msg % args) 75 | else: 76 | print(msg) 77 | 78 | 79 | def log(window, msg, *args): 80 | """Add a log message.""" 81 | global logs 82 | wlog = logs.setdefault(window.id(), WindowLog()) 83 | wlog.add_message(msg, args) 84 | 85 | 86 | def clear_log(window): 87 | """Clear log messages.""" 88 | try: 89 | logs[window.id()].clear() 90 | except KeyError: 91 | pass 92 | 93 | 94 | class RustOpenLog(sublime_plugin.WindowCommand): 95 | 96 | """Opens a view to display log messages generated by the Rust Enhanced 97 | plugin.""" 98 | 99 | def run(self): 100 | wlog = logs.setdefault(self.window.id(), WindowLog()) 101 | if wlog.view: 102 | self.window.focus_view(wlog.view) 103 | else: 104 | wlog.open_view(self.window) 105 | 106 | 107 | class RustLogEvent(sublime_plugin.ViewEventListener): 108 | 109 | @classmethod 110 | def is_applicable(cls, settings): 111 | return settings.has('rust_log_view') 112 | 113 | def on_pre_close(self): 114 | try: 115 | wlog = logs[self.view.settings().get('rust_log_view')] 116 | except KeyError: 117 | return 118 | else: 119 | wlog.view = None 120 | -------------------------------------------------------------------------------- /rust/opanel.py: -------------------------------------------------------------------------------- 1 | """Module displaying build output in a Sublime output panel.""" 2 | 3 | import sublime 4 | 5 | import os 6 | import re 7 | from . import rust_proc, messages, util, semver, levels, log 8 | 9 | # Use the same panel name that Sublime's build system uses so that "Show Build 10 | # Results" will open the same panel. I don't see any particular reason why 11 | # this would be a problem. If it is, it's a simple matter of changing this. 12 | PANEL_NAME = 'exec' 13 | 14 | # Pattern used for finding location of panics in test output. 15 | # 16 | # Rust 1.73 changed the formatting of a panic message. 17 | # Older versions looked like: 18 | # thread 'basic_error1' panicked at 'assertion failed: false', tests/test_test_output.rs:9:5 19 | # 1.73 changed it to look like: 20 | # thread 'basic_error1' panicked at tests/test_test_output.rs:9:5: 21 | # assertion failed: false 22 | PANIC_PATTERN = r'(?:, |panicked at )([^,<\n]*\.[A-z]{2}):([0-9]+)' 23 | 24 | def create_output_panel(window, base_dir): 25 | output_view = window.create_output_panel(PANEL_NAME) 26 | s = output_view.settings() 27 | if util.get_setting('show_errors_inline', True): 28 | # FILENAME:LINE: MESSAGE 29 | # Two dots to handle Windows DRIVE: 30 | s.set('result_file_regex', '^[^:]+: (..[^:]*):([0-9]+): (.*)$') 31 | else: 32 | build_pattern = '^[ \\t]*-->[ \\t]*([^<\n]*):([0-9]+):([0-9]+)' 33 | pattern = '(?|%s|%s)' % (build_pattern, PANIC_PATTERN) 34 | s.set('result_file_regex', pattern) 35 | # Used for resolving relative paths. 36 | s.set('result_base_dir', base_dir) 37 | s.set('word_wrap', True) # XXX Or False? 38 | s.set('line_numbers', False) 39 | s.set('gutter', False) 40 | s.set('scroll_past_end', False) 41 | output_view.assign_syntax('Cargo.sublime-syntax') 42 | # 'color_scheme'? 43 | # XXX: Is this necessary? 44 | # self.window.create_output_panel(PANEL_NAME) 45 | if util.get_setting('show_panel_on_build', True): 46 | window.run_command('show_panel', {'panel': 'output.' + PANEL_NAME}) 47 | return output_view 48 | 49 | 50 | def display_message(window, msg): 51 | """Utility function for displaying a one-off message (typically an error) 52 | in a new output panel.""" 53 | v = create_output_panel(window, '') 54 | _append(v, msg) 55 | 56 | 57 | def _append(view, text): 58 | view.run_command('append', {'characters': text, 59 | 'scroll_to_end': True}) 60 | 61 | 62 | class OutputListener(rust_proc.ProcListener): 63 | 64 | """Listener used for displaying results to a Sublime output panel.""" 65 | 66 | # Sublime view used for output. 67 | output_view = None 68 | 69 | def __init__(self, window, base_path, command_name, rustc_version): 70 | self.window = window 71 | self.base_path = base_path 72 | self.command_name = command_name 73 | self.rustc_version = rustc_version 74 | self.rendered = [] 75 | 76 | def on_begin(self, proc): 77 | self.output_view = create_output_panel(self.window, self.base_path) 78 | self._append('[Running: %s]' % (' '.join(proc.cmd),)) 79 | 80 | def on_data(self, proc, data): 81 | region_start = self.output_view.size() 82 | self._append(data, nl=False) 83 | # Check for test errors. 84 | if self.command_name == 'test': 85 | # Re-fetch the data to handle things like \t expansion. 86 | appended = self.output_view.substr( 87 | sublime.Region(region_start, self.output_view.size())) 88 | # This pattern also includes column numbers (which Sublime's 89 | # result_file_regex doesn't support). 90 | m = re.search(PANIC_PATTERN + r':([0-9]+)', appended) 91 | if m: 92 | path = os.path.join(self.base_path, m.group(1)) 93 | if not os.path.exists(path): 94 | # Panics outside of the crate display a path to that 95 | # crate's source file (such as libcore), which is probably 96 | # not available. 97 | return 98 | message = messages.Message() 99 | lineno = int(m.group(2)) - 1 100 | # Region columns appear to the left, so this is +1. 101 | col = int(m.group(3)) 102 | # Rust 1.24 changed column numbering to be 1-based. 103 | if semver.match(self.rustc_version, '>=1.24.0-beta'): 104 | col -= 1 105 | message.span = ((lineno, col), (lineno, col)) 106 | # +2 to skip ", " 107 | build_region = sublime.Region(region_start + m.start() + 2, 108 | region_start + m.end()) 109 | message.output_panel_region = build_region 110 | message.path = path 111 | message.level = levels.level_from_str('error') 112 | messages.add_message(self.window, message) 113 | 114 | def on_error(self, proc, message): 115 | self._append(message) 116 | 117 | def on_json(self, proc, obj): 118 | try: 119 | message = obj['message'] 120 | except KeyError: 121 | return 122 | messages.add_rust_messages(self.window, self.base_path, message, 123 | None, self.msg_cb) 124 | try: 125 | self.rendered.append(message['rendered']) 126 | except KeyError: 127 | pass 128 | 129 | def msg_cb(self, message): 130 | """Display the message in the output panel. Also marks the message 131 | with the output panel region where the message is shown. This allows 132 | us to scroll the output panel to the correct region when cycling 133 | through messages. 134 | """ 135 | if not message.text: 136 | # Region-only messages can be ignored. 137 | return 138 | region_start = self.output_view.size() + len(message.level.name) + 2 139 | path = message.path 140 | if path: 141 | if self.base_path and path.startswith(self.base_path): 142 | path = os.path.relpath(path, self.base_path) 143 | if message.span: 144 | highlight_text = '%s:%d' % (path, message.span[0][0] + 1) 145 | else: 146 | highlight_text = path 147 | self._append('%s: %s: %s' % (message.level, highlight_text, message.text)) 148 | region = sublime.Region(region_start, 149 | region_start + len(highlight_text)) 150 | else: 151 | self._append('%s: %s' % (message.level, message.text)) 152 | region = sublime.Region(region_start) 153 | message.output_panel_region = region 154 | 155 | def on_finished(self, proc, rc): 156 | if rc: 157 | self._append('[Finished in %.1fs with exit code %d]' % ( 158 | proc.elapsed, rc)) 159 | self._display_debug(proc) 160 | else: 161 | self._append('[Finished in %.1fs]' % proc.elapsed) 162 | messages.messages_finished(self.window) 163 | # Tell Sublime to find all of the lines with pattern from 164 | # result_file_regex. 165 | self.output_view.find_all_results() 166 | win_info = messages.get_or_init_window_info(self.window) 167 | win_info['rendered'] = ''.join(self.rendered) 168 | 169 | def on_terminated(self, proc): 170 | self._append('[Build interrupted]') 171 | 172 | def _append(self, message, nl=True): 173 | if nl: 174 | message += '\n' 175 | _append(self.output_view, message) 176 | self.rendered.append(message) 177 | 178 | def _display_debug(self, proc): 179 | # Display some information to help the user debug any build problems. 180 | log.log(self.window, 'cwd: %s', proc.cwd) 181 | # TODO: Fix this when adding PATH/env support. 182 | log.log(self.window, 'path: %s', proc.env.get('PATH')) 183 | -------------------------------------------------------------------------------- /rust/rust_proc.py: -------------------------------------------------------------------------------- 1 | """Module for running cargo or rustc and parsing the output. 2 | 3 | It is assumed a thread only ever has one process running at a time. 4 | """ 5 | 6 | import json 7 | import os 8 | import re 9 | import signal 10 | import subprocess 11 | import sys 12 | import threading 13 | import time 14 | import shellenv 15 | import sublime 16 | import traceback 17 | 18 | from . import util, log 19 | 20 | # Map Sublime window ID to RustProc. 21 | PROCS = {} 22 | PROCS_LOCK = threading.Lock() 23 | 24 | # Environment (as s dict) from the user's login shell. 25 | USER_SHELL_ENV = None 26 | 27 | 28 | class ProcessTerminatedError(Exception): 29 | """Process was terminated by another thread.""" 30 | 31 | 32 | class ProcListener(object): 33 | 34 | """Listeners are used to handle output events while a process is 35 | running.""" 36 | 37 | def on_begin(self, proc): 38 | """Called just before the process is started.""" 39 | pass 40 | 41 | def on_data(self, proc, data): 42 | """A line of text output by the process.""" 43 | pass 44 | 45 | def on_error(self, proc, message): 46 | """Called when there is an error, such as failure to decode utf-8.""" 47 | log.critical(sublime.active_window(), 'Rust Error: %s', message) 48 | 49 | def on_json(self, proc, obj): 50 | """Parsed JSON output from the command.""" 51 | pass 52 | 53 | def on_finished(self, proc, rc): 54 | """Called after all output has been processed.""" 55 | pass 56 | 57 | def on_terminated(self, proc): 58 | """Called when the process is terminated by another thread. Note that 59 | the process may still be running.""" 60 | pass 61 | 62 | 63 | class SlurpListener(ProcListener): 64 | 65 | def on_begin(self, proc): 66 | self.json = [] 67 | self.data = [] 68 | 69 | def on_json(self, proc, obj): 70 | self.json.append(obj) 71 | 72 | def on_data(self, proc, data): 73 | self.data.append(data) 74 | 75 | 76 | def _slurp(window, cmd, cwd): 77 | p = RustProc() 78 | listener = SlurpListener() 79 | p.run(window, cmd, cwd, listener) 80 | rc = p.wait() 81 | return (rc, listener) 82 | 83 | 84 | def slurp_json(window, cmd, cwd): 85 | """Run a command and return the JSON output from it. 86 | 87 | :param window: Sublime window. 88 | :param cmd: The command to run (list of strings). 89 | :param cwd: The directory where to run the command. 90 | 91 | :returns: List of parsed JSON objects. 92 | 93 | :raises ProcessTermiantedError: Process was terminated by another thread. 94 | :raises OSError: Failed to launch the child process. `FileNotFoundError` 95 | is a typical example if the executable is not found. 96 | """ 97 | rc, listener = _slurp(window, cmd, cwd) 98 | if not listener.json and rc: 99 | log.critical(window, 'Failed to run: %s', cmd) 100 | log.critical(window, ''.join(listener.data)) 101 | return listener.json 102 | 103 | 104 | def check_output(window, cmd, cwd): 105 | """Run a command and return the text output from it. 106 | 107 | :param window: Sublime window. 108 | :param cmd: The command to run (list of strings). 109 | :param cwd: The directory where to run the command. 110 | 111 | :returns: A string of the command's output. 112 | 113 | :raises ProcessTermiantedError: Process was terminated by another thread. 114 | :raises OSError: Failed to launch the child process. `FileNotFoundError` 115 | is a typical example if the executable is not found. 116 | :raises subprocess.CalledProcessError: The command returned a nonzero exit 117 | status. 118 | """ 119 | rc, listener = _slurp(window, cmd, cwd) 120 | output = ''.join(listener.data) 121 | if rc: 122 | raise subprocess.CalledProcessError(rc, cmd, output) 123 | return output 124 | 125 | 126 | class RustProc(object): 127 | 128 | """Launches and controls a subprocess.""" 129 | 130 | # Set to True when the process is finished running. 131 | finished = False 132 | # Set to True if the process was forcefully terminated. 133 | terminated = False 134 | # Command to run as a list of strings. 135 | cmd = None 136 | # The directory where the command is being run. 137 | cwd = None 138 | # Environment dictionary used in the child. 139 | env = None 140 | # subprocess.Popen object 141 | proc = None 142 | # Time when the process was started. 143 | start_time = None 144 | # Number of seconds it took to run. 145 | elapsed = None 146 | # The thread used for reading output. 147 | _stdout_thread = None 148 | 149 | def run(self, window, cmd, cwd, listener, env=None, 150 | decode_json=True, json_stop_pattern=None): 151 | """Run the process. 152 | 153 | :param window: Sublime window. 154 | :param cmd: The command to run (list of strings). 155 | :param cwd: The directory where to run the command. 156 | :param listener: `ProcListener` to receive the output. 157 | :param env: Dictionary of environment variables to add. 158 | :param decode_json: If True, will check for lines starting with `{` to 159 | decode as a JSON message. 160 | :param json_stop_pattern: Regular expression used to detect when it 161 | should stop looking for JSON messages. This is used by `cargo 162 | run` so that it does not capture output from the user's program 163 | that might start with an open curly brace. 164 | 165 | :raises ProcessTermiantedError: Process was terminated by another 166 | thread. 167 | :raises OSError: Failed to launch the child process. 168 | `FileNotFoundError` is a typical example if the executable is not 169 | found. 170 | """ 171 | self.cmd = cmd 172 | self.cwd = cwd 173 | self.listener = listener 174 | self.start_time = time.time() 175 | self.window = window 176 | self.decode_json = decode_json 177 | self.json_stop_pattern = json_stop_pattern 178 | 179 | from . import rust_thread 180 | try: 181 | t = rust_thread.THREADS[window.id()] 182 | except KeyError: 183 | pass 184 | else: 185 | if t.should_exit: 186 | raise ProcessTerminatedError() 187 | 188 | with PROCS_LOCK: 189 | PROCS[window.id()] = self 190 | listener.on_begin(self) 191 | 192 | # Configure the environment. 193 | self.env = os.environ.copy() 194 | if util.get_setting('rust_include_shell_env', True): 195 | global USER_SHELL_ENV 196 | if USER_SHELL_ENV is None: 197 | USER_SHELL_ENV = shellenv.get_env()[1] 198 | self.env.update(USER_SHELL_ENV) 199 | 200 | rust_env = util.get_setting('rust_env') 201 | if rust_env: 202 | for k, v in rust_env.items(): 203 | rust_env[k] = os.path.expandvars(v) 204 | self.env.update(rust_env) 205 | 206 | if env: 207 | self.env.update(env) 208 | 209 | log.log(window, 'Running: %s', ' '.join(self.cmd)) 210 | 211 | if sys.platform == 'win32': 212 | # Prevent a console window from popping up. 213 | startupinfo = subprocess.STARTUPINFO() 214 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 215 | self.proc = subprocess.Popen( 216 | self.cmd, 217 | cwd=self.cwd, 218 | env=self.env, 219 | startupinfo=startupinfo, 220 | stdin=subprocess.PIPE, 221 | stdout=subprocess.PIPE, 222 | stderr=subprocess.STDOUT, 223 | ) 224 | else: 225 | # Make the process the group leader so we can easily kill all its 226 | # children. 227 | self.proc = subprocess.Popen( 228 | self.cmd, 229 | cwd=self.cwd, 230 | preexec_fn=os.setpgrp, 231 | env=self.env, 232 | stdin=subprocess.PIPE, 233 | stdout=subprocess.PIPE, 234 | stderr=subprocess.STDOUT, 235 | ) 236 | 237 | self._stdout_thread = threading.Thread(target=self._read_stdout, 238 | name='%s: Stdout' % (threading.current_thread().name,)) 239 | self._stdout_thread.start() 240 | 241 | def terminate(self): 242 | """Kill the process. 243 | 244 | Termination may not happen immediately. Use wait() if you need to 245 | ensure when it is finished. 246 | """ 247 | if self.finished or self.proc is None: 248 | return 249 | self.finished = True 250 | self.terminated = True 251 | if sys.platform == 'win32': 252 | # Use taskkill to kill the entire tree (terminate only kills top 253 | # process). 254 | startupinfo = subprocess.STARTUPINFO() 255 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 256 | # /T - Kill tree 257 | # /F - Force kill 258 | subprocess.Popen( 259 | 'taskkill /T /F /PID ' + str(self.proc.pid), 260 | startupinfo=startupinfo) 261 | else: 262 | # Must kill the entire process group. The rustup wrapper won't 263 | # forward the signal to cargo, and cargo doesn't forward signals 264 | # to any processes it spawns. 265 | os.killpg(os.getpgid(self.proc.pid), signal.SIGTERM) 266 | # stdout reader should catch the end of the stream and perform 267 | # cleanup. 268 | self.listener.on_terminated(self) 269 | 270 | def wait(self): 271 | """Wait for the process to finish. 272 | 273 | :return: The returncode of the process. 274 | 275 | :raises ProcessTerminatedError: Process was interrupted by another 276 | thread. 277 | """ 278 | # stdout_thread is responsible for cleanup, setting `finished`, etc. 279 | if self._stdout_thread: 280 | self._stdout_thread.join() 281 | rc = self.proc.wait() 282 | if self.terminated: 283 | raise ProcessTerminatedError() 284 | return rc 285 | 286 | def _read_stdout(self): 287 | while True: 288 | line = self.proc.stdout.readline() 289 | if not line: 290 | rc = self._cleanup() 291 | self.listener.on_finished(self, rc) 292 | break 293 | try: 294 | line = line.decode('utf-8') 295 | except: 296 | self.listener.on_error(self, 297 | '[Error decoding UTF-8: %r]' % line) 298 | continue 299 | if self.decode_json and line.startswith('{'): 300 | try: 301 | result = json.loads(line) 302 | except: 303 | self.listener.on_error(self, 304 | '[Error loading JSON from rust: %r]' % line) 305 | else: 306 | try: 307 | self.listener.on_json(self, result) 308 | except: 309 | self.listener.on_error(self, 310 | 'Rust Enhanced Internal Error: %s' % ( 311 | traceback.format_exc(),)) 312 | else: 313 | if self.json_stop_pattern and \ 314 | re.match(self.json_stop_pattern, line): 315 | # Stop looking for JSON open curly bracket. 316 | self.decode_json = False 317 | if line.startswith('--- stderr'): 318 | # Rust 1.19 had a bug 319 | # (https://github.com/rust-lang/cargo/issues/4223) where 320 | # it was incorrectly printing stdout from the compiler 321 | # (fixed in 1.20). 322 | self.decode_json = False 323 | # Sublime always uses \n internally. 324 | line = line.replace('\r\n', '\n') 325 | self.listener.on_data(self, line) 326 | 327 | def _cleanup(self): 328 | self.elapsed = time.time() - self.start_time 329 | self.finished = True 330 | self._stdout_thread = None 331 | self.proc.stdout.close() 332 | rc = self.proc.wait() 333 | with PROCS_LOCK: 334 | p = PROCS.get(self.window.id()) 335 | if p is self: 336 | del PROCS[self.window.id()] 337 | return rc 338 | -------------------------------------------------------------------------------- /rust/rust_thread.py: -------------------------------------------------------------------------------- 1 | """Manage threads used for running Rust processes.""" 2 | 3 | from . import util, rust_proc 4 | 5 | import sublime 6 | import threading 7 | 8 | # Map Sublime window ID to RustThread. 9 | THREADS = {} 10 | THREADS_LOCK = threading.Lock() 11 | 12 | 13 | class RustThread(object): 14 | 15 | """A thread for running Rust processes. 16 | 17 | Subclasses should implement run to define the code to run. 18 | 19 | Subclasses should check `should_exit` around any long-running steps. 20 | """ 21 | 22 | # threading.Thread instance 23 | thread = None 24 | # If this is true, then it is OK to kill this thread to start a new one. 25 | silently_interruptible = True 26 | # Set to True when the thread should terminate. 27 | should_exit = False 28 | # Sublime window this thread is attached to. 29 | window = None 30 | # Name of the thread. 31 | name = None 32 | 33 | def __init__(self, window): 34 | self.window = window 35 | 36 | def start(self): 37 | """Start the thread.""" 38 | self.thread = threading.Thread(name=self.name, 39 | target=self._thread_run) 40 | self.thread.start() 41 | 42 | @property 43 | def current_proc(self): 44 | """The current `RustProc` being executed by this thread, or None.""" 45 | return rust_proc.PROCS.get(self.window.id(), None) 46 | 47 | def describe(self): 48 | """Returns a string with the name of the thread.""" 49 | p = self.current_proc 50 | if p: 51 | return '%s: %s' % (self.name, ' '.join(p.cmd)) 52 | else: 53 | return self.name 54 | 55 | def _thread_run(self): 56 | # Determine if this thread is allowed to run. 57 | while True: 58 | with THREADS_LOCK: 59 | t = THREADS.get(self.window.id(), None) 60 | if not t or not t.is_alive(): 61 | THREADS[self.window.id()] = self 62 | break 63 | 64 | # Another thread is already running for this window. 65 | if t.should_exit: 66 | t.join() 67 | elif t.silently_interruptible: 68 | t.terminate() 69 | t.join() 70 | elif self.silently_interruptible: 71 | # Never allowed to interrupt. 72 | return 73 | else: 74 | # Neither is interruptible (the user started a Build 75 | # while one is already running). 76 | msg = """ 77 | Rust Build 78 | 79 | The following Rust command is still running, do you want to cancel it? 80 | %s""" % self.describe() 81 | if sublime.ok_cancel_dialog(util.multiline_fix(msg), 82 | 'Stop Running Command'): 83 | t.terminate() 84 | t.join() 85 | else: 86 | # Allow the original process to finish. 87 | return 88 | # Try again. 89 | 90 | try: 91 | self.run() 92 | finally: 93 | with THREADS_LOCK: 94 | t = THREADS.get(self.window.id(), None) 95 | if t is self: 96 | del THREADS[self.window.id()] 97 | 98 | def run(self): 99 | raise NotImplementedError() 100 | 101 | def terminate(self): 102 | """Asks the thread to exit. 103 | 104 | If the thread is running a process, the process will be killed. 105 | """ 106 | self.should_exit = True 107 | p = self.current_proc 108 | if p and not p.finished: 109 | p.terminate() 110 | 111 | def is_alive(self): 112 | return self.thread.is_alive() 113 | 114 | def join(self, timeout=None): 115 | return self.thread.join(timeout=timeout) 116 | -------------------------------------------------------------------------------- /rust/semver.py: -------------------------------------------------------------------------------- 1 | # https://github.com/k-bx/python-semver 2 | # 3 | # Copyright (c) 2013, Konstantine Rybnikov 4 | # All rights reserved. 5 | # 6 | # Redistribution and use in source and binary forms, with or without modification, 7 | # are permitted provided that the following conditions are met: 8 | # 9 | # Redistributions of source code must retain the above copyright notice, this 10 | # list of conditions and the following disclaimer. 11 | # 12 | # Redistributions in binary form must reproduce the above copyright notice, this 13 | # list of conditions and the following disclaimer in the documentation and/or 14 | # other materials provided with the distribution. 15 | # 16 | # Neither the name of the {organization} nor the names of its 17 | # contributors may be used to endorse or promote products derived from 18 | # this software without specific prior written permission. 19 | # 20 | # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | # DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 24 | # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 27 | # ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | """ 32 | Python helper for Semantic Versioning (http://semver.org/) 33 | """ 34 | 35 | import collections 36 | import re 37 | import sys 38 | 39 | 40 | __version__ = '2.7.5' 41 | __author__ = 'Kostiantyn Rybnikov' 42 | __author_email__ = 'k-bx@k-bx.com' 43 | 44 | _REGEX = re.compile( 45 | r""" 46 | ^ 47 | (?P(?:0|[1-9][0-9]*)) 48 | \. 49 | (?P(?:0|[1-9][0-9]*)) 50 | \. 51 | (?P(?:0|[1-9][0-9]*)) 52 | (\-(?P 53 | (?:0|[1-9A-Za-z-][0-9A-Za-z-]*) 54 | (\.(?:0|[1-9A-Za-z-][0-9A-Za-z-]*))* 55 | ))? 56 | (\+(?P 57 | [0-9A-Za-z-]+ 58 | (\.[0-9A-Za-z-]+)* 59 | ))? 60 | $ 61 | """, re.VERBOSE) 62 | 63 | _LAST_NUMBER = re.compile(r'(?:[^\d]*(\d+)[^\d]*)+') 64 | 65 | if not hasattr(__builtins__, 'cmp'): 66 | def cmp(a, b): 67 | return (a > b) - (a < b) 68 | 69 | 70 | def parse(version): 71 | """Parse version to major, minor, patch, pre-release, build parts. 72 | 73 | :param version: version string 74 | :return: dictionary with the keys 'build', 'major', 'minor', 'patch', 75 | and 'prerelease'. The prerelease or build keys can be None 76 | if not provided 77 | :rtype: dict 78 | """ 79 | match = _REGEX.match(version) 80 | if match is None: 81 | raise ValueError('%s is not valid SemVer string' % version) 82 | 83 | version_parts = match.groupdict() 84 | 85 | version_parts['major'] = int(version_parts['major']) 86 | version_parts['minor'] = int(version_parts['minor']) 87 | version_parts['patch'] = int(version_parts['patch']) 88 | 89 | return version_parts 90 | 91 | 92 | VersionInfo = collections.namedtuple( 93 | 'VersionInfo', 'major minor patch prerelease build') 94 | 95 | # Only change it for Python > 3 as it is readonly 96 | # for version 2 97 | if sys.version_info > (3, 0): 98 | VersionInfo.__doc__ = """ 99 | :param int major: version when you make incompatible API changes. 100 | :param int minor: version when you add functionality in 101 | a backwards-compatible manner. 102 | :param int patch: version when you make backwards-compatible bug fixes. 103 | :param str prerelease: an optional prerelease string 104 | :param str build: an optional build string 105 | 106 | >>> import semver 107 | >>> ver = semver.parse('3.4.5-pre.2+build.4') 108 | >>> ver 109 | {'build': 'build.4', 'major': 3, 'minor': 4, 'patch': 5, 110 | 'prerelease': 'pre.2'} 111 | """ 112 | 113 | 114 | def parse_version_info(version): 115 | """Parse version string to a VersionInfo instance. 116 | 117 | :param version: version string 118 | :return: a :class:`VersionInfo` instance 119 | :rtype: :class:`VersionInfo` 120 | """ 121 | parts = parse(version) 122 | version_info = VersionInfo( 123 | parts['major'], parts['minor'], parts['patch'], 124 | parts['prerelease'], parts['build']) 125 | 126 | return version_info 127 | 128 | 129 | def compare(ver1, ver2): 130 | """Compare two versions 131 | 132 | :param ver1: version string 1 133 | :param ver2: version string 2 134 | :return: The return value is negative if ver1 < ver2, 135 | zero if ver1 == ver2 and strictly positive if ver1 > ver2 136 | :rtype: int 137 | """ 138 | def nat_cmp(a, b): 139 | def convert(text): 140 | return int(text) if re.match('[0-9]+', text) else text 141 | 142 | def split_key(key): 143 | return [convert(c) for c in key.split('.')] 144 | 145 | def cmp_prerelease_tag(a, b): 146 | if isinstance(a, int) and isinstance(b, int): 147 | return cmp(a, b) 148 | elif isinstance(a, int): 149 | return -1 150 | elif isinstance(b, int): 151 | return 1 152 | else: 153 | return cmp(a, b) 154 | 155 | a, b = a or '', b or '' 156 | a_parts, b_parts = split_key(a), split_key(b) 157 | for sub_a, sub_b in zip(a_parts, b_parts): 158 | cmp_result = cmp_prerelease_tag(sub_a, sub_b) 159 | if cmp_result != 0: 160 | return cmp_result 161 | else: 162 | return cmp(len(a), len(b)) 163 | 164 | def compare_by_keys(d1, d2): 165 | for key in ['major', 'minor', 'patch']: 166 | v = cmp(d1.get(key), d2.get(key)) 167 | if v: 168 | return v 169 | 170 | rc1, rc2 = d1.get('prerelease'), d2.get('prerelease') 171 | rccmp = nat_cmp(rc1, rc2) 172 | 173 | if not rccmp: 174 | return 0 175 | if not rc1: 176 | return 1 177 | elif not rc2: 178 | return -1 179 | 180 | return rccmp 181 | 182 | v1, v2 = parse(ver1), parse(ver2) 183 | 184 | return compare_by_keys(v1, v2) 185 | 186 | 187 | def match(version, match_expr): 188 | """Compare two versions through a comparison 189 | 190 | :param str version: a version string 191 | :param str match_expr: operator and version; valid operators are 192 | < smaller than 193 | > greater than 194 | >= greator or equal than 195 | <= smaller or equal than 196 | == equal 197 | != not equal 198 | :return: True if the expression matches the version, otherwise False 199 | :rtype: bool 200 | """ 201 | prefix = match_expr[:2] 202 | if prefix in ('>=', '<=', '==', '!='): 203 | match_version = match_expr[2:] 204 | elif prefix and prefix[0] in ('>', '<'): 205 | prefix = prefix[0] 206 | match_version = match_expr[1:] 207 | else: 208 | raise ValueError("match_expr parameter should be in format , " 209 | "where is one of " 210 | "['<', '>', '==', '<=', '>=', '!=']. " 211 | "You provided: %r" % match_expr) 212 | 213 | possibilities_dict = { 214 | '>': (1,), 215 | '<': (-1,), 216 | '==': (0,), 217 | '!=': (-1, 1), 218 | '>=': (0, 1), 219 | '<=': (-1, 0) 220 | } 221 | 222 | possibilities = possibilities_dict[prefix] 223 | cmp_res = compare(version, match_version) 224 | 225 | return cmp_res in possibilities 226 | 227 | 228 | def max_ver(ver1, ver2): 229 | """Returns the greater version of two versions 230 | 231 | :param ver1: version string 1 232 | :param ver2: version string 2 233 | :return: the greater version of the two 234 | :rtype: :class:`VersionInfo` 235 | """ 236 | cmp_res = compare(ver1, ver2) 237 | if cmp_res == 0 or cmp_res == 1: 238 | return ver1 239 | else: 240 | return ver2 241 | 242 | 243 | def min_ver(ver1, ver2): 244 | """Returns the smaller version of two versions 245 | 246 | :param ver1: version string 1 247 | :param ver2: version string 2 248 | :return: the smaller version of the two 249 | :rtype: :class:`VersionInfo` 250 | """ 251 | cmp_res = compare(ver1, ver2) 252 | if cmp_res == 0 or cmp_res == -1: 253 | return ver1 254 | else: 255 | return ver2 256 | 257 | 258 | def format_version(major, minor, patch, prerelease=None, build=None): 259 | """Format a version according to the Semantic Versioning specification 260 | 261 | :param str major: the required major part of a version 262 | :param str minor: the required minor part of a version 263 | :param str patch: the required patch part of a version 264 | :param str prerelease: the optional prerelease part of a version 265 | :param str build: the optional build part of a version 266 | :return: the formatted string 267 | :rtype: str 268 | """ 269 | version = "%d.%d.%d" % (major, minor, patch) 270 | if prerelease is not None: 271 | version = version + "-%s" % prerelease 272 | 273 | if build is not None: 274 | version = version + "+%s" % build 275 | 276 | return version 277 | 278 | 279 | def _increment_string(string): 280 | """ 281 | Look for the last sequence of number(s) in a string and increment, from: 282 | http://code.activestate.com/recipes/442460-increment-numbers-in-a-string/#c1 283 | """ 284 | match = _LAST_NUMBER.search(string) 285 | if match: 286 | next_ = str(int(match.group(1)) + 1) 287 | start, end = match.span(1) 288 | string = string[:max(end - len(next_), start)] + next_ + string[end:] 289 | return string 290 | 291 | 292 | def bump_major(version): 293 | """Raise the major part of the version 294 | 295 | :param: version string 296 | :return: the raised version string 297 | :rtype: str 298 | """ 299 | verinfo = parse(version) 300 | return format_version(verinfo['major'] + 1, 0, 0) 301 | 302 | 303 | def bump_minor(version): 304 | """Raise the minor part of the version 305 | 306 | :param: version string 307 | :return: the raised version string 308 | :rtype: str 309 | """ 310 | verinfo = parse(version) 311 | return format_version(verinfo['major'], verinfo['minor'] + 1, 0) 312 | 313 | 314 | def bump_patch(version): 315 | """Raise the patch part of the version 316 | 317 | :param: version string 318 | :return: the raised version string 319 | :rtype: str 320 | """ 321 | verinfo = parse(version) 322 | return format_version(verinfo['major'], verinfo['minor'], 323 | verinfo['patch'] + 1) 324 | 325 | 326 | def bump_prerelease(version): 327 | """Raise the prerelease part of the version 328 | 329 | :param: version string 330 | :return: the raised version string 331 | :rtype: str 332 | """ 333 | verinfo = parse(version) 334 | verinfo['prerelease'] = _increment_string(verinfo['prerelease'] or 'rc.0') 335 | return format_version(verinfo['major'], verinfo['minor'], verinfo['patch'], 336 | verinfo['prerelease']) 337 | 338 | 339 | def bump_build(version): 340 | """Raise the build part of the version 341 | 342 | :param: version string 343 | :return: the raised version string 344 | :rtype: str 345 | """ 346 | verinfo = parse(version) 347 | verinfo['build'] = _increment_string(verinfo['build'] or 'build.0') 348 | return format_version(verinfo['major'], verinfo['minor'], verinfo['patch'], 349 | verinfo['prerelease'], verinfo['build']) 350 | -------------------------------------------------------------------------------- /rust/target_detect.py: -------------------------------------------------------------------------------- 1 | """Used to determine the Cargo targets from any given .rs file. 2 | 3 | This is very imperfect. It uses heuristics to try to detect targets. This 4 | could be significantly improved by using "rustc --emit dep-info", however, 5 | that may be a little tricky (and doesn't work on errors). See 6 | https://github.com/rust-lang/cargo/issues/3211 7 | """ 8 | 9 | import os 10 | from . import rust_proc, util, log 11 | 12 | 13 | class TargetDetector(object): 14 | 15 | def __init__(self, window): 16 | self.window = window 17 | 18 | def determine_targets(self, file_name, metadata=None): 19 | """Detect the target/filters needed to pass to Cargo to compile 20 | file_name. 21 | Returns list of (target_src_path, target_command_line_args) tuples. 22 | 23 | :keyword metadata: Output from `get_cargo_metadata`. If None, will run 24 | it manually. 25 | 26 | :raises ProcessTerminatedError: Thread should shut down. 27 | """ 28 | # Try checking for target match in settings. 29 | result = self._targets_manual_config(file_name) 30 | if result: 31 | return result 32 | 33 | # Try a heuristic to detect the filename. 34 | if metadata is None: 35 | metadata = util.get_cargo_metadata(self.window, os.path.dirname(file_name)) 36 | if not metadata: 37 | return [] 38 | # Each "workspace" shows up as a separate package. 39 | for package in metadata['packages']: 40 | root_path = os.path.dirname(package['manifest_path']) 41 | targets = package['targets'] 42 | # targets is list of dictionaries: 43 | # {'kind': ['lib'], 44 | # 'name': 'target-name', 45 | # 'src_path': 'path/to/lib.rs'} 46 | # src_path may be absolute or relative, fix it. 47 | for target in targets: 48 | if not os.path.isabs(target['src_path']): 49 | target['src_path'] = os.path.join(root_path, target['src_path']) 50 | target['src_path'] = os.path.normpath(target['src_path']) 51 | 52 | # Try exact filename matches. 53 | result = self._targets_exact_match(targets, file_name) 54 | if result: 55 | return result 56 | 57 | # No exact match, try to find all targets with longest matching 58 | # parent directory. 59 | result = self._targets_longest_matches(targets, file_name) 60 | if result: 61 | return result 62 | 63 | log.log(self.window, 64 | 'Rust Enhanced: Failed to find target for %r', file_name) 65 | return [(None, [])] 66 | 67 | def _targets_manual_config(self, file_name): 68 | """Check for Cargo targets in the Sublime settings.""" 69 | # First check config for manual targets. 70 | for project in util.get_setting('projects', {}).values(): 71 | src_root = os.path.join(project.get('root', ''), 'src') 72 | if not file_name.startswith(src_root): 73 | continue 74 | targets = project.get('targets', {}) 75 | for tfile, tcmd in targets.items(): 76 | if file_name == os.path.join(src_root, tfile): 77 | return [(tfile, tcmd.split())] 78 | else: 79 | target = targets.get('_default', '') 80 | if target: 81 | # Unfortunately don't have the target src filename. 82 | return [('', target)] 83 | return None 84 | 85 | def _target_to_args(self, target): 86 | """Convert target from Cargo metadata to Cargo command-line argument. 87 | """ 88 | # Targets have multiple "kinds" when you specify crate-type in 89 | # Cargo.toml, like: 90 | # crate-type = ["rlib", "dylib"] 91 | # 92 | # Libraries are the only thing that support this at this time, and 93 | # generally you only use one command-line argument to build multiple 94 | # "kinds" (--lib in this case). 95 | # 96 | # Caution: [[example]] that specifies crate-type had issues before 97 | # 1.17. 98 | # See https://github.com/rust-lang/cargo/pull/3556 and 99 | # https://github.com/rust-lang/cargo/issues/3572 100 | # https://github.com/rust-lang/cargo/pull/3668 (ISSUE FIXED) 101 | # 102 | # For now, just grab the first kind since it will always result in the 103 | # same arguments. 104 | kind = target['kind'][0] 105 | if kind in ('lib', 'rlib', 'dylib', 'cdylib', 'staticlib', 'proc-macro'): 106 | return (target['src_path'], ['--lib']) 107 | elif kind in ('bin', 'test', 'example', 'bench'): 108 | return (target['src_path'], ['--' + kind, target['name']]) 109 | elif kind in ('custom-build',): 110 | # Currently no way to target build.rs explicitly. 111 | # Or, run rustc (without cargo) on build.rs. 112 | # TODO: "cargo check" seems to work 113 | return None 114 | else: 115 | # Unknown kind, don't know how to build. 116 | raise ValueError(kind) 117 | 118 | def _targets_exact_match(self, targets, file_name): 119 | """Check for Cargo targets that exactly match the current file.""" 120 | for target in targets: 121 | if target['src_path'] == file_name: 122 | args = self._target_to_args(target) 123 | if args: 124 | return [args] 125 | return None 126 | 127 | def _targets_longest_matches(self, targets, file_name): 128 | """Determine the Cargo targets that are in the same directory (or 129 | parent) of the current file.""" 130 | result = [] 131 | # Find longest path match. 132 | # TODO: This is sub-optimal, because it may result in multiple targets. 133 | # Consider using the output of rustc --emit dep-info. 134 | # See https://github.com/rust-lang/cargo/issues/3211 for some possible 135 | # problems with that. 136 | path_match = os.path.dirname(file_name) 137 | found = False 138 | found_lib = False 139 | found_bin = False 140 | # Sort the targets by name for consistent results, which cargo doesn't 141 | # guarantee. 142 | targets.sort(key=lambda x: x['name']) 143 | while not found: 144 | for target in targets: 145 | if os.path.dirname(target['src_path']) == path_match: 146 | target_args = self._target_to_args(target) 147 | if target_args: 148 | result.append(target_args) 149 | found = True 150 | if target_args[1][0] == '--bin': 151 | found_bin = True 152 | if target_args[1][0] == '--lib': 153 | found_lib = True 154 | p = os.path.dirname(path_match) 155 | if p == path_match: 156 | # Root path 157 | break 158 | path_match = p 159 | # If the match is both --bin and --lib in the same directory, 160 | # just do --bin. 161 | if found_bin and found_lib: 162 | result = [x for x in result if x[1][0] != '--bin'] 163 | return result 164 | -------------------------------------------------------------------------------- /rust/themes.py: -------------------------------------------------------------------------------- 1 | """Themes for different message styles.""" 2 | 3 | from . import util 4 | from .batch import * 5 | 6 | 7 | POPUP_CSS = 'body { margin: 0.25em; }' 8 | 9 | 10 | def _help_link(code): 11 | if code: 12 | return ' ?' % ( 13 | code,) 14 | else: 15 | return '' 16 | 17 | 18 | class Theme: 19 | 20 | """Base class for themes.""" 21 | 22 | def render(self, view, batch, for_popup=False): 23 | """Return a minihtml string of the content in the message batch.""" 24 | raise NotImplementedError() 25 | 26 | 27 | class ClearTheme(Theme): 28 | 29 | """Theme with a clear background, and colors matching the user's color 30 | scheme.""" 31 | 32 | TMPL = util.multiline_fix(""" 33 | 71 | 72 | {content} 73 | 74 | """) 75 | 76 | MSG_TMPL = util.multiline_fix(""" 77 |
78 | {level_text}{text}{help_link}{close_link} 79 |
80 | """) 81 | 82 | LINK_TMPL = util.multiline_fix(""" 83 | 86 | """) 87 | 88 | def render(self, view, batch, for_popup=False): 89 | if for_popup: 90 | extra_css = POPUP_CSS 91 | else: 92 | extra_css = '' 93 | 94 | # Collect all the messages for this batch. 95 | msgs = [] 96 | last_level = None 97 | for i, msg in enumerate(batch): 98 | if msg.hidden: 99 | continue 100 | text = msg.escaped_text(view, '') 101 | if not text: 102 | continue 103 | if msg.suggested_replacement is not None: 104 | level_text = '' 105 | else: 106 | if msg.level == last_level: 107 | level_text = ' ' * (len(str(msg.level)) + 2) 108 | else: 109 | level_text = '%s: ' % (msg.level,) 110 | last_level = msg.level 111 | if i == 0: 112 | # Only show close link on first message of a batch. 113 | close_link = ' \xD7' 114 | else: 115 | close_link = '' 116 | msgs.append(self.MSG_TMPL.format( 117 | level=msg.level, 118 | level_text=level_text, 119 | text=text, 120 | help_link=_help_link(msg.code), 121 | close_link=close_link, 122 | )) 123 | 124 | # Add cross-links. 125 | if isinstance(batch, PrimaryBatch): 126 | for url, path in batch.child_links: 127 | msgs.append(self.LINK_TMPL.format( 128 | url=url, text=see_also(url), path=path)) 129 | else: 130 | if batch.back_link: 131 | msgs.append(self.LINK_TMPL.format( 132 | url=batch.back_link[0], 133 | text='See Primary:', 134 | path=batch.back_link[1])) 135 | 136 | return self.TMPL.format( 137 | error_color=util.get_setting('rust_syntax_error_color'), 138 | warning_color=util.get_setting('rust_syntax_warning_color'), 139 | note_color=util.get_setting('rust_syntax_note_color'), 140 | help_color=util.get_setting('rust_syntax_help_color'), 141 | content=''.join(msgs), 142 | extra_css=extra_css) 143 | 144 | 145 | class SolidTheme(Theme): 146 | 147 | """Theme with a solid background color.""" 148 | 149 | TMPL = util.multiline_fix(""" 150 | 208 | 209 | {content} 210 | 211 | """) 212 | 213 | PRIMARY_MSG_TMPL = util.multiline_fix(""" 214 |
215 | {icon} {text}{help_link} \xD7 216 | {children} 217 | {links} 218 |
219 | """) 220 | 221 | SECONDARY_MSG_TMPL = util.multiline_fix(""" 222 |
223 | {children} 224 | {links} 225 |
226 | """) 227 | 228 | CHILD_TMPL = util.multiline_fix(""" 229 |
{icon} {text}
230 | """) 231 | 232 | LINK_TMPL = util.multiline_fix(""" 233 | 234 | """) 235 | 236 | def render(self, view, batch, for_popup=False): 237 | 238 | def icon(level): 239 | # minihtml does not support switching resolution for images based on DPI. 240 | # Always use the @2x images, and downscale on 1x displays. It doesn't 241 | # look as good, but is close enough. 242 | # See https://github.com/SublimeTextIssues/Core/issues/2228 243 | path = util.icon_path(level, res=2) 244 | if not path: 245 | return '' 246 | else: 247 | return '' % (path,) 248 | 249 | if for_popup: 250 | extra_css = POPUP_CSS 251 | else: 252 | extra_css = '' 253 | 254 | # Collect all the child messages together. 255 | children = [] 256 | for child in batch.children: 257 | if child.hidden: 258 | continue 259 | # Don't show the icon for children with the same level as the 260 | # primary message. 261 | if isinstance(batch, PrimaryBatch) and child.level == batch.primary_message.level: 262 | child_icon = icon('none') 263 | else: 264 | child_icon = icon(child.level) 265 | minihtml_text = child.escaped_text(view, ' ' + icon('none')) 266 | if minihtml_text: 267 | txt = self.CHILD_TMPL.format(level=child.level, 268 | icon=child_icon, 269 | text=minihtml_text) 270 | children.append(txt) 271 | 272 | if isinstance(batch, PrimaryBatch): 273 | links = [] 274 | for url, path in batch.child_links: 275 | links.append( 276 | self.LINK_TMPL.format( 277 | url=url, text=see_also(url), path=path)) 278 | text = batch.primary_message.escaped_text(view, ' ' + icon('none')) 279 | if not text and not children: 280 | return None 281 | content = self.PRIMARY_MSG_TMPL.format( 282 | level=batch.primary_message.level, 283 | icon=icon(batch.primary_message.level), 284 | text=text, 285 | help_link=_help_link(batch.primary_message.code), 286 | children=''.join(children), 287 | links=''.join(links)) 288 | else: 289 | if batch.back_link: 290 | link = self.LINK_TMPL.format(url=batch.back_link[0], 291 | text='See Primary:', 292 | path=batch.back_link[1]) 293 | else: 294 | link = '' 295 | content = self.SECONDARY_MSG_TMPL.format( 296 | level=batch.primary_batch.primary_message.level, 297 | icon=icon(batch.primary_batch.primary_message.level), 298 | children=''.join(children), 299 | links=link) 300 | 301 | return self.TMPL.format(content=content, extra_css=extra_css) 302 | 303 | 304 | class TestTheme(Theme): 305 | 306 | """Theme used by tests for verifying which messages are displayed.""" 307 | 308 | def __init__(self): 309 | self.path_messages = {} 310 | 311 | def render(self, view, batch, for_popup=False): 312 | from .messages import Message 313 | messages = self.path_messages.setdefault(batch.first().path, []) 314 | for msg in batch: 315 | # Region-only messages will get checked by the region-checking 316 | # code. 317 | if msg.text or msg.suggested_replacement is not None: 318 | messages.append(msg) 319 | 320 | # Create fake messages for the links to simplify the test code. 321 | def add_fake(msg, text): 322 | fake = Message() 323 | fake.text = text 324 | fake.span = msg.span 325 | fake.path = msg.path 326 | fake.level = '' 327 | messages.append(fake) 328 | 329 | if isinstance(batch, PrimaryBatch): 330 | for url, path in batch.child_links: 331 | add_fake(batch.primary_message, see_also(url) + ' ' + path) 332 | else: 333 | if batch.back_link: 334 | add_fake(batch.first(), 'See Primary: ' + batch.back_link[1]) 335 | return None 336 | 337 | 338 | THEMES = { 339 | 'clear': ClearTheme(), 340 | 'solid': SolidTheme(), 341 | 'test': TestTheme(), 342 | } 343 | 344 | def see_also(path): 345 | if path.endswith(':external'): 346 | return 'See Also (external):' 347 | else: 348 | return 'See Also:' 349 | -------------------------------------------------------------------------------- /rust/util.py: -------------------------------------------------------------------------------- 1 | """General utilities used by the Rust package.""" 2 | 3 | import sublime 4 | import textwrap 5 | import os 6 | 7 | 8 | PACKAGE_NAME = __package__.split('.')[0] 9 | 10 | 11 | def index_with(l, cb): 12 | """Find the index of a value in a sequence using a callback. 13 | 14 | :param l: The sequence to search. 15 | :param cb: Function to call, should return true if the given value matches 16 | what you are searching for. 17 | :returns: Returns the index of the match, or -1 if no match. 18 | """ 19 | for i, v in enumerate(l): 20 | if cb(v): 21 | return i 22 | return -1 23 | 24 | 25 | def multiline_fix(s): 26 | """Remove indentation from a multi-line string.""" 27 | return textwrap.dedent(s).lstrip() 28 | 29 | 30 | def get_setting(name, default=None): 31 | """Retrieve a setting from Sublime settings.""" 32 | pdata = sublime.active_window().project_data() 33 | if pdata: 34 | v = pdata.get('settings', {}).get(name) 35 | if v is not None: 36 | return v 37 | settings = sublime.load_settings('RustEnhanced.sublime-settings') 38 | v = settings.get(name) 39 | if v is not None: 40 | return v 41 | settings = sublime.load_settings('Preferences.sublime-settings') 42 | # XXX: Also check "Distraction Free"? 43 | return settings.get(name, default) 44 | 45 | 46 | def get_rustc_version(window, cwd, toolchain=None): 47 | """Returns the rust version for the given directory. 48 | 49 | :Returns: A string such as '1.16.0' or '1.17.0-nightly'. 50 | """ 51 | from . import rust_proc 52 | cmd = ['rustc'] 53 | if toolchain: 54 | cmd.append('+' + toolchain) 55 | cmd.append('--version') 56 | output = rust_proc.check_output(window, cmd, cwd) 57 | # Example outputs: 58 | # rustc 1.15.1 (021bd294c 2017-02-08) 59 | # rustc 1.16.0-beta.2 (bc15d5281 2017-02-16) 60 | # rustc 1.17.0-nightly (306035c21 2017-02-18) 61 | return output.split()[1] 62 | 63 | 64 | def find_cargo_manifest(path): 65 | """Find the Cargo.toml file in the given path, or any of its parents. 66 | 67 | :Returns: The path where Cargo.toml is found, or None. 68 | """ 69 | path = os.path.normpath(path) 70 | if os.path.isfile(path): 71 | path = os.path.dirname(path) 72 | while True: 73 | manifest = os.path.join(path, 'Cargo.toml') 74 | if os.path.exists(manifest): 75 | return path 76 | parent = os.path.dirname(path) 77 | if parent == path: 78 | return None 79 | path = parent 80 | 81 | 82 | def active_view_is_rust(window=None, view=None): 83 | """Determine if the current view is a Rust source file. 84 | 85 | :param window: The Sublime window (defaults to active window). 86 | :param view: The view to check (defaults to active view). 87 | 88 | :Returns: True if it is a Rust source file, False if not. 89 | """ 90 | if view is None: 91 | if window is None: 92 | window = sublime.active_window() 93 | view = window.active_view() 94 | if not view: 95 | return False 96 | # Require it to be saved to disk. 97 | if not view.file_name(): 98 | return False 99 | return 'source.rust' in view.scope_name(0) 100 | 101 | 102 | def is_rust_view(settings): 103 | """Helper for use with ViewEventListener.""" 104 | s = settings.get('syntax') 105 | return (s == 'Packages/%s/RustEnhanced.sublime-syntax' % (PACKAGE_NAME,)) 106 | 107 | 108 | def get_cargo_metadata(window, cwd, toolchain=None): 109 | """Load Cargo metadata. 110 | 111 | :returns: None on failure, otherwise a dictionary from Cargo: 112 | - packages: List of packages: 113 | - name 114 | - manifest_path: Path to Cargo.toml. 115 | - targets: List of target dictionaries: 116 | - name: Name of target. 117 | - src_path: Path of top-level source file. May be a 118 | relative path. 119 | - kind: List of kinds. May contain multiple entries if 120 | `crate-type` specifies multiple values in Cargo.toml. 121 | Lots of different types of values: 122 | - Libraries: 'lib', 'rlib', 'dylib', 'cdylib', 'staticlib', 123 | 'proc-macro' 124 | - Executables: 'bin', 'test', 'example', 'bench' 125 | - build.rs: 'custom-build' 126 | 127 | :raises ProcessTermiantedError: Process was terminated by another thread. 128 | """ 129 | from . import rust_proc 130 | cmd = ['cargo'] 131 | if toolchain: 132 | cmd.append('+' + toolchain) 133 | cmd.extend(['metadata', '--no-deps']) 134 | output = rust_proc.slurp_json(window, 135 | cmd, 136 | cwd=cwd) 137 | if output: 138 | return output[0] 139 | else: 140 | return None 141 | 142 | 143 | def icon_path(level, res=None): 144 | """Return a path to a message-level icon.""" 145 | level = str(level) 146 | if level not in ('error', 'warning', 'note', 'help', 'none'): 147 | return '' 148 | gutter_style = get_setting('rust_gutter_style', 'shape') 149 | if gutter_style == 'none': 150 | return '' 151 | else: 152 | if res: 153 | res_suffix = '@%ix' % (res,) 154 | else: 155 | res_suffix = '' 156 | return 'Packages/%s/images/gutter/%s-%s%s.png' % ( 157 | PACKAGE_NAME, gutter_style, level, res_suffix) 158 | 159 | 160 | def open_views_for_file(window, file_name): 161 | """Return all views for the given file name.""" 162 | view = window.find_open_file(file_name) 163 | if view is None: 164 | return [] 165 | 166 | return [v for v in window.views() if v.buffer_id() == view.buffer_id()] 167 | -------------------------------------------------------------------------------- /snippets/Err.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | Err 4 | source.rust 5 | Err(…) 6 | 7 | -------------------------------------------------------------------------------- /snippets/Ok.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | Ok 4 | source.rust 5 | Ok(…) 6 | 7 | -------------------------------------------------------------------------------- /snippets/Some.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | Some 4 | source.rust 5 | Some(…) 6 | 7 | -------------------------------------------------------------------------------- /snippets/allow.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | allow 4 | source.rust 5 | #[allow(…)] 6 | 7 | -------------------------------------------------------------------------------- /snippets/assert.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | assert 6 | source.rust 7 | assert!(…) 8 | 9 | -------------------------------------------------------------------------------- /snippets/assert_eq.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | assert_eq 6 | source.rust 7 | assert_eq!(…, …) 8 | 9 | -------------------------------------------------------------------------------- /snippets/attribute.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | # 4 | source.rust 5 | 6 | -------------------------------------------------------------------------------- /snippets/bench.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 7 | bench 8 | source.rust 9 | 10 | -------------------------------------------------------------------------------- /snippets/break.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | break 4 | source.rust meta.block.rust 5 | break; 6 | -------------------------------------------------------------------------------- /snippets/const.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | const 4 | source.rust 5 | const …: … = …; 6 | 7 | -------------------------------------------------------------------------------- /snippets/continue.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | continue 4 | source.rust meta.block.rust 5 | continue; 6 | -------------------------------------------------------------------------------- /snippets/dbg.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | source.rust 4 | dbg 5 | dbg!(…) 6 | -------------------------------------------------------------------------------- /snippets/debug.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | source.rust 4 | debug 5 | debug!(…) 6 | -------------------------------------------------------------------------------- /snippets/deny.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | deny 4 | source.rust 5 | #[deny(…)] 6 | 7 | -------------------------------------------------------------------------------- /snippets/derive.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | derive 4 | source.rust 5 | #[derive(…)] 6 | 7 | -------------------------------------------------------------------------------- /snippets/else.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | else 6 | source.rust 7 | else { … } 8 | 9 | -------------------------------------------------------------------------------- /snippets/enum.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 7 | enum 8 | source.rust 9 | enum … { … } 10 | 11 | -------------------------------------------------------------------------------- /snippets/eprintln.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | source.rust 4 | eprintln 5 | eprintln!(…) 6 | 7 | -------------------------------------------------------------------------------- /snippets/error.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | source.rust 4 | error 5 | error!(…) 6 | -------------------------------------------------------------------------------- /snippets/extern-crate.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | extern-crate 4 | source.rust 5 | extern crate …; 6 | 7 | -------------------------------------------------------------------------------- /snippets/extern-fn.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | ${4:RetType} { 3 | ${0:// add code here} 4 | }]]> 5 | extern-fn 6 | source.rust 7 | extern "C" fn …(…) { … } 8 | 9 | -------------------------------------------------------------------------------- /snippets/extern-mod.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | extern-mod 6 | source.rust 7 | extern "C" { … } 8 | 9 | -------------------------------------------------------------------------------- /snippets/feature.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | feature 4 | source.rust 5 | #![feature(…)] 6 | 7 | -------------------------------------------------------------------------------- /snippets/fn.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | :)/}${4:RetType}${4/(^.+$)|^$/(?1: :)/}{ 3 | ${0:unimplemented!()} 4 | }]]> 5 | fn 6 | source.rust 7 | fn …(…) { … } 8 | 9 | -------------------------------------------------------------------------------- /snippets/for.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | for 6 | source.rust 7 | for … in … { … } 8 | 9 | -------------------------------------------------------------------------------- /snippets/format.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | format 4 | source.rust 5 | format!(…) 6 | 7 | -------------------------------------------------------------------------------- /snippets/if-let.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | if-let 6 | source.rust 7 | if let … = … { … } 8 | 9 | -------------------------------------------------------------------------------- /snippets/if.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | if 6 | source.rust 7 | if … { … } 8 | 9 | -------------------------------------------------------------------------------- /snippets/impl-trait.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | impl-trait 6 | source.rust 7 | impl … for … { … } 8 | 9 | -------------------------------------------------------------------------------- /snippets/impl.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | impl 6 | source.rust 7 | impl … { … } 8 | 9 | -------------------------------------------------------------------------------- /snippets/info.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | source.rust 4 | info 5 | info!(…) 6 | -------------------------------------------------------------------------------- /snippets/let-mut.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | letm 4 | source.rust 5 | let mut … = …; 6 | 7 | -------------------------------------------------------------------------------- /snippets/let.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | let 4 | source.rust 5 | let … = …; 6 | 7 | -------------------------------------------------------------------------------- /snippets/loop.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | loop 6 | source.rust 7 | loop { … } 8 | 9 | -------------------------------------------------------------------------------- /snippets/macro_export.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | macro 6 | source.rust 7 | [macro_export] 8 | 9 | -------------------------------------------------------------------------------- /snippets/macro_rules.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | {${0}}; 4 | }]]> 5 | macro_rules 6 | source.rust 7 | macro_rules! … { … } 8 | 9 | -------------------------------------------------------------------------------- /snippets/macro_use.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | macro 6 | source.rust 7 | [macro_use] 8 | 9 | -------------------------------------------------------------------------------- /snippets/main.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | main 6 | source.rust 7 | fn main() { … } 8 | 9 | -------------------------------------------------------------------------------- /snippets/match.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | ${3:expr}, 4 | ${4:None} => ${5:expr}, 5 | }]]> 6 | match 7 | source.rust 8 | match … { … } 9 | 10 | -------------------------------------------------------------------------------- /snippets/mod.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | mod 6 | source.rust 7 | mod … { … } 8 | 9 | -------------------------------------------------------------------------------- /snippets/panic.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | panic 4 | source.rust 5 | panic!(…) 6 | 7 | -------------------------------------------------------------------------------- /snippets/plugin.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | plugin 4 | source.rust 5 | #![plugin(…)] 6 | 7 | -------------------------------------------------------------------------------- /snippets/println.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | source.rust 4 | println 5 | println!(…) 6 | 7 | -------------------------------------------------------------------------------- /snippets/repr-c.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | repr 6 | source.rust 7 | repr(C) 8 | 9 | -------------------------------------------------------------------------------- /snippets/static.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | static 4 | source.rust 5 | static …: … = …; 6 | 7 | -------------------------------------------------------------------------------- /snippets/struct-tuple.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | struct-tuple 4 | source.rust 5 | struct …(…); 6 | 7 | -------------------------------------------------------------------------------- /snippets/struct-unit.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | struct-unit 4 | source.rust 5 | struct …; 6 | 7 | -------------------------------------------------------------------------------- /snippets/struct.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 6 | struct 7 | source.rust 8 | struct … { … } 9 | 10 | -------------------------------------------------------------------------------- /snippets/test.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 7 | test 8 | source.rust 9 | 10 | -------------------------------------------------------------------------------- /snippets/tests-mod.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 12 | tests-mod 13 | source.rust 14 | 15 | -------------------------------------------------------------------------------- /snippets/trace.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | source.rust 4 | trace 5 | trace!(…) 6 | -------------------------------------------------------------------------------- /snippets/trait.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 6 | trait 7 | source.rust 8 | trait … { … } 9 | 10 | -------------------------------------------------------------------------------- /snippets/type.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | type 4 | source.rust 5 | type … = …; 6 | 7 | -------------------------------------------------------------------------------- /snippets/unimplemented.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | unimplemented 6 | source.rust 7 | unimplemented!(…) 8 | 9 | -------------------------------------------------------------------------------- /snippets/unreachable.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | unreachable 6 | source.rust 7 | unreachable!(…) 8 | 9 | -------------------------------------------------------------------------------- /snippets/use.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | use 4 | source.rust 5 | use …; 6 | 7 | -------------------------------------------------------------------------------- /snippets/warn.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 3 | source.rust 4 | warn 5 | warn!(…) 6 | -------------------------------------------------------------------------------- /snippets/while-let.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | while-let 6 | source.rust 7 | while let … = … { … } 8 | 9 | -------------------------------------------------------------------------------- /snippets/while.sublime-snippet: -------------------------------------------------------------------------------- 1 | 2 | 5 | while 6 | source.rust 7 | while … { … } 8 | 9 | -------------------------------------------------------------------------------- /toggle_setting.py: -------------------------------------------------------------------------------- 1 | import sublime_plugin 2 | from .rust import (util, messages) 3 | 4 | 5 | class ToggleRustSyntaxSettingCommand(sublime_plugin.WindowCommand): 6 | 7 | """Toggles on-save checking for the current window.""" 8 | 9 | def run(self): 10 | # Grab the setting and reverse it. 11 | window = self.window 12 | current_state = util.get_setting('rust_syntax_checking', True) 13 | new_state = not current_state 14 | pdata = window.project_data() 15 | pdata.setdefault('settings', {})['rust_syntax_checking'] = new_state 16 | if not new_state: 17 | messages.clear_messages(window) 18 | window.status_message("Rust syntax checking is now " + ("inactive" if current_state else "active")) 19 | window.set_project_data(pdata) 20 | 21 | def is_checked(self): 22 | return util.get_setting('rust_syntax_checking', True) 23 | --------------------------------------------------------------------------------