├── .no-sublime-package ├── .gitignore ├── demo.gif ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap ├── Default (Windows).sublime-keymap ├── LanguageTool.sublime-settings ├── LTServer.py ├── LanguageList.py ├── Main.sublime-menu ├── LanguageTool.sublime-commands ├── README.md └── LanguageTool.py /.no-sublime-package: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | 3 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gtarawneh/languagetool-sublime/HEAD/demo.gif -------------------------------------------------------------------------------- /Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+shift+c"], "command": "language_tool" }, 3 | { "keys": ["alt+down"], "command": "goto_next_language_problem", "args": { "jump_forward": true } }, 4 | { "keys": ["alt+up"], "command": "goto_next_language_problem", "args": { "jump_forward": false } }, 5 | { "keys": ["alt+shift+f"], "command": "mark_language_problem_solved", "args": { "apply_fix": true } }, 6 | { "keys": ["alt+d"], "command": "mark_language_problem_solved", "args": { "apply_fix": false } } 7 | ] 8 | -------------------------------------------------------------------------------- /Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+shift+c"], "command": "language_tool" }, 3 | { "keys": ["alt+down"], "command": "goto_next_language_problem", "args": { "jump_forward": true } }, 4 | { "keys": ["alt+up"], "command": "goto_next_language_problem", "args": { "jump_forward": false } }, 5 | { "keys": ["alt+shift+f"], "command": "mark_language_problem_solved", "args": { "apply_fix": true } }, 6 | { "keys": ["alt+d"], "command": "mark_language_problem_solved", "args": { "apply_fix": false } } 7 | ] 8 | -------------------------------------------------------------------------------- /Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { "keys": ["ctrl+shift+c"], "command": "language_tool" }, 3 | { "keys": ["alt+down"], "command": "goto_next_language_problem", "args": { "jump_forward": true } }, 4 | { "keys": ["alt+up"], "command": "goto_next_language_problem", "args": { "jump_forward": false } }, 5 | { "keys": ["alt+shift+f"], "command": "mark_language_problem_solved", "args": { "apply_fix": true } }, 6 | { "keys": ["alt+d"], "command": "mark_language_problem_solved", "args": { "apply_fix": false } } 7 | ] 8 | -------------------------------------------------------------------------------- /LanguageTool.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | "languagetool_server_remote": "https://api.languagetool.org/v2/check", 3 | "languagetool_server_local": "http://localhost:8081/v2/check", 4 | "default_server": "remote", 5 | "display_mode": "panel", 6 | "languagetool_jar": "C:\\bin\\LanguageTool-3.5\\languagetool.jar", 7 | "highlight-scope": "comment", 8 | "ignored-scopes": [ 9 | "support.function.*.latex", 10 | "meta.*.latex", 11 | "comment.*.tex", 12 | "keyword.control.tex" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /LTServer.py: -------------------------------------------------------------------------------- 1 | import sublime 2 | import json 3 | 4 | def _is_ST2(): 5 | return (int(sublime.version()) < 3000) 6 | 7 | if _is_ST2(): 8 | from urllib import urlencode 9 | from urllib import urlopen 10 | else: 11 | try: 12 | from urlparse import urlencode 13 | from urllib2 import urlopen 14 | except ImportError: 15 | from urllib.parse import urlencode 16 | from urllib.request import urlopen 17 | 18 | def getResponse(server, text, language, disabledRules): 19 | payload = { 20 | 'language': language, 21 | 'text': text.encode('utf8'), 22 | 'User-Agent': 'sublime', 23 | 'disabledRules' : ','.join(disabledRules) 24 | } 25 | content = _post(server, payload) 26 | if content: 27 | j = json.loads(content.decode('utf-8')) 28 | return j['matches'] 29 | else: 30 | return None 31 | 32 | # internal functions: 33 | 34 | def _post(server, payload): 35 | data = urlencode(payload).encode('utf8') 36 | try: 37 | content = urlopen(server, data).read() 38 | return content 39 | except IOError: 40 | return None 41 | -------------------------------------------------------------------------------- /LanguageList.py: -------------------------------------------------------------------------------- 1 | # languages is an array of tuples in the format 2 | # (name, code) 3 | 4 | languages = [ 5 | ("Autodetect Language", "auto"), 6 | ("Asturian", "ast"), 7 | ("Belarusian", "be"), 8 | ("Breton", "br"), 9 | ("Catalan", "ca"), 10 | ("ValencianCatalan", "ca"), 11 | ("Danish", "da"), 12 | ("German (Austria)", "de-AT"), 13 | ("German", "de"), 14 | ("German (Germany)", "de-DE"), 15 | ("German (Swiss)", "de-CH"), 16 | ("Greek", "el"), 17 | ("English (American)", "en-US"), 18 | ("English (Australian)", "en-AU"), 19 | ("English (GB)", "en-GB"), 20 | ("English (Canadian)", "en-CA"), 21 | ("English", "en"), 22 | ("English (New Zealand)", "en-NZ"), 23 | ("English (South African)", "en-ZA"), 24 | ("Esperanto", "eo"), 25 | ("Spanish", "es"), 26 | ("Persian", "fa"), 27 | ("French", "fr"), 28 | ("Galician", "gl"), 29 | ("Icelandic", "is"), 30 | ("Italian", "it"), 31 | ("Japanese", "ja"), 32 | ("Khmer", "km"), 33 | ("Lithuanian", "lt"), 34 | ("Malayalam", "ml"), 35 | ("Dutch", "nl"), 36 | ("Polish", "pl"), 37 | ("Portuguese (Brazil)", "pt-BR"), 38 | ("Portuguese (Portugal)", "pt-PT"), 39 | ("Portuguese", "pt"), 40 | ("Romanian", "ro"), 41 | ("Russian", "ru"), 42 | ("Slovak", "sk"), 43 | ("Slovenian", "sl"), 44 | ("Swedish", "sv"), 45 | ("Tamil", "ta"), 46 | ("Tagalog", "tl"), 47 | ("Ukrainian", "uk"), 48 | ("Chinese", "zh"), 49 | ]; 50 | 51 | -------------------------------------------------------------------------------- /Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "mnemonic": "n", 4 | "caption": "Preferences", 5 | "id": "preferences", 6 | "children": [ 7 | { 8 | "mnemonic": "P", 9 | "caption": "Package Settings", 10 | "id": "package-settings", 11 | "children": [ 12 | { 13 | "caption": "LanguageTool", 14 | "children": [ 15 | { 16 | "caption": "Settings – Default", 17 | "args": { 18 | "file": "${packages}/LanguageTool/LanguageTool.sublime-settings" 19 | }, 20 | "command": "open_file" 21 | }, 22 | { 23 | "caption": "Settings – User", 24 | "args": { 25 | "file": "${packages}/User/LanguageTool.sublime-settings" 26 | }, 27 | "command": "open_file" 28 | }, 29 | { 30 | "caption": "-" 31 | } 32 | ] 33 | } 34 | ] 35 | } 36 | ] 37 | } 38 | ] -------------------------------------------------------------------------------- /LanguageTool.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "LanguageTool: Check Text", 4 | "command": "language_tool" 5 | }, 6 | { 7 | "caption": "LanguageTool: Check Text (Local Server)", 8 | "command": "language_tool", 9 | "args": {"force_server": "local"} 10 | }, 11 | { 12 | "caption": "LanguageTool: Check Text (Remote Server)", 13 | "command": "language_tool", 14 | "args": {"force_server": "remote"} 15 | }, 16 | { 17 | "caption": "LanguageTool: Next Problem", 18 | "command": "goto_next_language_problem", 19 | "args": {"jump_forward": true} 20 | }, 21 | { 22 | "caption": "LanguageTool: Previous Problem", 23 | "command": "goto_next_language_problem", 24 | "args": {"jump_forward": false} 25 | }, 26 | { 27 | "caption": "LanguageTool: Apply Suggested Correction(s)", 28 | "command": "mark_language_problem_solved", 29 | "args": {"apply_fix": true} 30 | }, 31 | { 32 | "caption": "LanguageTool: Ignore Problem", 33 | "command": "mark_language_problem_solved", 34 | "args": {"apply_fix": false} 35 | }, 36 | { 37 | "caption": "LanguageTool: Clear Problems", 38 | "command": "clear_language_problems" 39 | }, 40 | { 41 | "caption": "LanguageTool: Change Language", 42 | "command": "change_language_tool_language" 43 | }, 44 | { 45 | "caption": "LanguageTool: Deactivate Rule", 46 | "command": "deactivate_rule" 47 | }, 48 | { 49 | "caption": "LanguageTool: Activate Rule", 50 | "command": "activate_rule" 51 | }, 52 | { 53 | "caption": "LanguageTool: Start Local Server", 54 | "command": "start_language_tool_server" 55 | } 56 | ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### LanguageTool for Sublime Text 2/3 2 | 3 | #### Overview 4 | 5 | This is a simple adapter to integrate 6 | [LanguageTool](https://languagetool.org/) (an open source proof-reading 7 | program) into Sublime Text 2/3. 8 | 9 | From : 10 | 11 | > LanguageTool is an Open Source proof­reading program for English, French, 12 | > German, Polish, and more than 20 other languages. It finds many errors that 13 | > a simple spell checker cannot detect and several grammar problems. 14 | 15 | ![languagetol-sublime demo](https://cdn.rawgit.com/gtarawneh/languagetool-sublime/master/demo.gif) 16 | 17 | #### Installation 18 | 19 | If you're using Package Control then open up the command palette 20 | (ctrl+shift+p), type `install`, press Enter then type 21 | `languagetool` and press Enter again. 22 | 23 | To get the latest updates before they get released, install via `Package 24 | Control: Add Repository`. This will update your plugin with new commits as 25 | they are being pushed to the repo. 26 | 27 | #### Usage 28 | 29 | Open the file you want to proof-read then: 30 | 31 | 1. Run a language check (ctrl+shift+c). Any problems identified by LanguageTool will be highlighted. 32 | 2. Move between the problems using alt+down (next) and alt+up (previous). 33 | 3. A panel at the bottom will display a brief description of the highlighted problem and suggest corrections if available. 34 | 4. Begin typing to correct the selected problem or press alt+shift+f to apply the suggested correction. 35 | 5. To ignore a problem, press alt+d. 36 | 6. Auto-correcting a problem or ignoring it will move focus to the next problem. 37 | 38 | All commands and their keyboard shortcuts are in the command palette with the 39 | prefix `LanguageTool:`. 40 | 41 | #### Configuration 42 | 43 | The settings file for the plugin can be opened from the `Preferences` menu 44 | (`Preferences` → `Package Settings` → `LanguageTool` → 45 | `Settings - User`). Default settings are in the corresponding submenu item 46 | `Settings - Default`. Note that default settings are provided for reference 47 | and should not be edited as they may be overwritten when the plugin is updated 48 | or reinstalled. Instead, copy and modify any settings you wish to override to 49 | `Settings - User`. 50 | 51 | #### Local vs. Remote Checking 52 | 53 | The adapter supports local and remote LanguageTool servers. Remote checking is 54 | the default and works by submitting text over https to an api endpoint on 55 | (this can be changed in plugin settings). This public 56 | service is subject to usage constraints including: 57 | 58 | 1. 20 requests per IP per minute (this is supposed to be a peak value — don’t 59 | constantly send this many requests or LanguageTool would have to block you) 60 | 2. 75KB text per IP per minute 61 | 3. 20KB text per request 62 | 4. Only up to 30 misspelled words will have suggestions. 63 | 5. No guarantees about performance or availability. 64 | The limits may change at any time. 65 | 66 | (See for full details.) 67 | 68 | Instead of using the public (remote) LanguageTool service, text can be checked 69 | using a local LanguageTool Installation. A local LanguageTool server can be 70 | started by the plugin itself using the command `LanguageTool: Start Local 71 | Server` (this requires the settings entry `languagetool_jar` to point to the 72 | local languagetool JAR file), or from the command line following the 73 | instructions in . 74 | 75 | The settings file contains remote and local server URL entries. A third option 76 | `default_server` indicates which of these is used when the command 77 | `LanguageTool: Check Text` is ran. As an added convenience, two more commands: 78 | 79 | * `LanguageTool: Check Text (Local Server)` 80 | * `LanguageTool: Check Text (Remote Server)` 81 | 82 | are provided, which can be used to check text using the local/remote servers 83 | regardless of `default_server`. This can be used for one-off checks when it's 84 | desirable to use a particular server with certain pieces of text. 85 | 86 | #### License 87 | 88 | This plugin is freely available under 89 | [GPLv2](https://www.gnu.org/licenses/old-licenses/gpl-2.0.html) or later. 90 | 91 | #### Contributing 92 | 93 | Feel free to fork and improve. All contributions are welcome. 94 | -------------------------------------------------------------------------------- /LanguageTool.py: -------------------------------------------------------------------------------- 1 | """ 2 | LanguageTool.py 3 | 4 | This is a simple Sublime Text plugin for checking grammar. It passes buffer 5 | content to LanguageTool (via http) and highlights reported problems. 6 | """ 7 | 8 | import sublime 9 | import sublime_plugin 10 | import subprocess 11 | import os.path 12 | import fnmatch 13 | import itertools 14 | 15 | 16 | def _is_ST2(): 17 | return (int(sublime.version()) < 3000) 18 | 19 | 20 | if _is_ST2(): 21 | import LTServer 22 | import LanguageList 23 | else: 24 | from . import LTServer 25 | from . import LanguageList 26 | 27 | 28 | def move_caret(view, i, j): 29 | """Select character range [i, j] in view.""" 30 | target = view.text_point(0, i) 31 | view.sel().clear() 32 | view.sel().add(sublime.Region(target, target + j - i)) 33 | 34 | 35 | def set_status_bar(message): 36 | """Change status bar message.""" 37 | sublime.status_message(message) 38 | 39 | 40 | def select_problem(view, problem): 41 | reg = view.get_regions(problem['regionKey'])[0] 42 | move_caret(view, reg.a, reg.b) 43 | view.show_at_center(reg) 44 | show_problem(problem) 45 | 46 | 47 | def is_problem_solved(view, problem): 48 | """Return True iff a language problem has been resolved. 49 | 50 | A problem is considered resolved if either: 51 | 52 | 1. its region has zero length, or 53 | 2. its contents have been changed. 54 | """ 55 | rl = view.get_regions(problem['regionKey']) 56 | assert len(rl) > 0, 'tried to find non-existing region' 57 | region = rl[0] 58 | return region.empty() or (view.substr(region) != problem['orgContent']) 59 | 60 | 61 | def show_problem(p): 62 | """Show problem description and suggestions.""" 63 | 64 | def show_problem_panel(p): 65 | msg = p['message'] 66 | if p['replacements']: 67 | msg += '\n\nSuggestion(s): ' + ', '.join(p['replacements']) 68 | if p['urls']: 69 | msg += '\n\nMore Info: ' + '\n'.join(p['urls']) 70 | show_panel_text(msg) 71 | 72 | def show_problem_status_bar(p): 73 | if p['replacements']: 74 | msg = u"{0} ({1})".format(p['message'], p['replacements']) 75 | else: 76 | msg = p['message'] 77 | sublime.status_message(msg) 78 | 79 | # call appropriate show_problem function 80 | 81 | use_panel = get_settings().get('display_mode') == 'panel' 82 | show_fun = show_problem_panel if use_panel else show_problem_status_bar 83 | show_fun(p) 84 | 85 | 86 | def show_panel_text(text): 87 | window = sublime.active_window() 88 | if _is_ST2(): 89 | pt = window.get_output_panel("languagetool") 90 | pt.set_read_only(False) 91 | edit = pt.begin_edit() 92 | pt.insert(edit, pt.size(), text) 93 | window.run_command("show_panel", {"panel": "output.languagetool"}) 94 | else: 95 | window.run_command('set_language_tool_panel_text', {'str': text}) 96 | 97 | 98 | class setLanguageToolPanelTextCommand(sublime_plugin.TextCommand): 99 | def run(self, edit, str): 100 | window = sublime.active_window() 101 | pt = window.get_output_panel("languagetool") 102 | pt.settings().set("wrap_width", 0) 103 | pt.settings().set("word_wrap", True) 104 | pt.set_read_only(False) 105 | pt.run_command('insert', {'characters': str}) 106 | window.run_command("show_panel", {"panel": "output.languagetool"}) 107 | 108 | 109 | class gotoNextLanguageProblemCommand(sublime_plugin.TextCommand): 110 | def run(self, edit, jump_forward=True): 111 | v = self.view 112 | problems = v.__dict__.get("problems", []) 113 | if len(problems) > 0: 114 | sel = v.sel()[0] 115 | if jump_forward: 116 | for p in problems: 117 | r = v.get_regions(p['regionKey'])[0] 118 | if (not is_problem_solved(v, p)) and (sel.begin() < r.a): 119 | select_problem(v, p) 120 | return 121 | else: 122 | for p in reversed(problems): 123 | r = v.get_regions(p['regionKey'])[0] 124 | if (not is_problem_solved(v, p)) and (r.a < sel.begin()): 125 | select_problem(v, p) 126 | return 127 | set_status_bar("no further language problems to fix") 128 | sublime.active_window().run_command("hide_panel", { 129 | "panel": "output.languagetool" 130 | }) 131 | 132 | 133 | class clearLanguageProblemsCommand(sublime_plugin.TextCommand): 134 | def run(self, edit): 135 | v = self.view 136 | problems = v.__dict__.get("problems", []) 137 | for p in problems: 138 | v.erase_regions(p['regionKey']) 139 | problems = [] 140 | recompute_highlights(v) 141 | caretPos = self.view.sel()[0].end() 142 | v.sel().clear() 143 | sublime.active_window().run_command("hide_panel", { 144 | "panel": "output.languagetool" 145 | }) 146 | move_caret(v, caretPos, caretPos) 147 | 148 | 149 | class markLanguageProblemSolvedCommand(sublime_plugin.TextCommand): 150 | def run(self, edit, apply_fix): 151 | 152 | v = self.view 153 | 154 | problems = v.__dict__.get("problems", []) 155 | selected_region = v.sel()[0] 156 | 157 | # Find problem corresponding to selection 158 | for problem in problems: 159 | problem_region = v.get_regions(problem['regionKey'])[0] 160 | if problem_region == selected_region: 161 | break 162 | else: 163 | set_status_bar('no language problem selected') 164 | return 165 | 166 | next_caret_pos = problem_region.a 167 | replacements = problem['replacements'] 168 | 169 | if apply_fix and replacements: 170 | # fix selected problem: 171 | correct_problem(self.view, edit, problem, replacements) 172 | 173 | else: 174 | # ignore problem: 175 | equal_problems = get_equal_problems(problems, problem) 176 | for p2 in equal_problems: 177 | ignore_problem(p2, v, edit) 178 | # After ignoring problem: 179 | move_caret(v, next_caret_pos, next_caret_pos) # advance caret 180 | v.run_command("goto_next_language_problem") 181 | 182 | def choose_suggestion(view, p, replacements, choice): 183 | """Handle suggestion list selection.""" 184 | problems = view.__dict__.get("problems", []) 185 | if choice != -1: 186 | r = view.get_regions(p['regionKey'])[0] 187 | view.run_command('insert', {'characters': replacements[choice]}) 188 | c = r.a + len(replacements[choice]) 189 | move_caret(view, c, c) # move caret to end of region 190 | view.run_command("goto_next_language_problem") 191 | else: 192 | select_problem(view, p) 193 | 194 | 195 | def get_equal_problems(problems, x): 196 | """Find problems with same category and content as a given problem. 197 | 198 | Args: 199 | problems (list): list of problems to compare. 200 | x (dict): problem object to compare with. 201 | 202 | Returns: 203 | list: list of problems equal to x. 204 | 205 | """ 206 | 207 | def is_equal(prob1, prob2): 208 | same_category = prob1['category'] == prob2['category'] 209 | same_content = prob1['orgContent'] == prob2['orgContent'] 210 | return same_category and same_content 211 | 212 | return [problem for problem in problems if is_equal(problem, x)] 213 | 214 | 215 | def get_settings(): 216 | return sublime.load_settings('LanguageTool.sublime-settings') 217 | 218 | 219 | class startLanguageToolServerCommand(sublime_plugin.TextCommand): 220 | """Launch local LanguageTool Server.""" 221 | 222 | def run(self, edit): 223 | 224 | jar_path = get_settings().get('languagetool_jar') 225 | 226 | if not jar_path: 227 | show_panel_text("Setting languagetool_jar is undefined") 228 | return 229 | 230 | if not os.path.isfile(jar_path): 231 | show_panel_text( 232 | 'Error, could not find LanguageTool\'s JAR file (%s)' 233 | '\n\n' 234 | 'Please install LT in this directory' 235 | ' or modify the `languagetool_jar` setting.' % jar_path) 236 | return 237 | 238 | sublime.status_message('Starting local LanguageTool server ...') 239 | 240 | cmd = ['java', '-cp', jar_path, 'org.languagetool.server.HTTPServer', '--port', '8081'] 241 | 242 | if sublime.platform() == "windows": 243 | p = subprocess.Popen( 244 | cmd, 245 | stdin=subprocess.PIPE, 246 | stdout=subprocess.PIPE, 247 | stderr=subprocess.PIPE, 248 | shell=True, 249 | creationflags=subprocess.SW_HIDE) 250 | else: 251 | p = subprocess.Popen( 252 | cmd, 253 | stdin=subprocess.PIPE, 254 | stdout=subprocess.PIPE, 255 | stderr=subprocess.PIPE) 256 | 257 | 258 | class changeLanguageToolLanguageCommand(sublime_plugin.TextCommand): 259 | def run(self, edit): 260 | self.view.languages = LanguageList.languages 261 | languageNames = [x[0] for x in self.view.languages] 262 | handler = lambda ind: handle_language_selection(ind, self.view) 263 | self.view.window().show_quick_panel(languageNames, handler) 264 | 265 | 266 | def handle_language_selection(ind, view): 267 | key = 'language_tool_language' 268 | if ind == 0: 269 | view.settings().erase(key) 270 | else: 271 | selected_language = view.languages[ind][1] 272 | view.settings().set(key, selected_language) 273 | 274 | 275 | def correct_problem(view, edit, problem, replacements): 276 | 277 | def clear_and_advance(): 278 | clear_region(view, problem['regionKey']) 279 | move_caret(view, next_caret_pos, next_caret_pos) # advance caret 280 | view.run_command("goto_next_language_problem") 281 | 282 | if len(replacements) > 1: 283 | def callback_fun(i): 284 | choose_suggestion(view, problem, replacements, i) 285 | clear_and_advance() 286 | view.window().show_quick_panel(replacements, callback_fun) 287 | 288 | else: 289 | region = view.get_regions(problem['regionKey'])[0] 290 | view.replace(edit, region, replacements[0]) 291 | next_caret_pos = region.a + len(replacements[0]) 292 | clear_and_advance() 293 | 294 | 295 | def clear_region(view, region_key): 296 | r = view.get_regions(region_key)[0] 297 | dummyRg = sublime.Region(r.a, r.a) 298 | hscope = get_settings().get("highlight-scope", "comment") 299 | view.add_regions(region_key, [dummyRg], hscope, "", sublime.DRAW_OUTLINED) 300 | 301 | 302 | def ignore_problem(p, v, edit): 303 | clear_region(v, p['regionKey']) 304 | v.insert(edit, v.size(), "") # dummy edit to enable undoing ignore 305 | 306 | 307 | def load_ignored_rules(): 308 | ignored_rules_file = 'LanguageToolUser.sublime-settings' 309 | settings = sublime.load_settings(ignored_rules_file) 310 | return settings.get('ignored', []) 311 | 312 | 313 | def save_ignored_rules(ignored): 314 | ignored_rules_file = 'LanguageToolUser.sublime-settings' 315 | settings = sublime.load_settings(ignored_rules_file) 316 | settings.set('ignored', ignored) 317 | sublime.save_settings(ignored_rules_file) 318 | 319 | 320 | def get_server_url(settings, force_server): 321 | """Return LT server url based on settings. 322 | 323 | The returned url is for either the local or remote servers, defined by the 324 | settings entries: 325 | 326 | - language_server_local 327 | - language_server_remote 328 | 329 | The choice between the above is made based on the settings value 330 | 'default_server'. If not None, `force_server` will override this setting. 331 | 332 | """ 333 | server_setting = force_server or settings.get('default_server') 334 | setting_name = 'languagetool_server_%s' % server_setting 335 | server = settings.get(setting_name) 336 | return server 337 | 338 | 339 | class LanguageToolCommand(sublime_plugin.TextCommand): 340 | def run(self, edit, force_server=None): 341 | 342 | settings = get_settings() 343 | server_url = get_server_url(settings, force_server) 344 | ignored_scopes = settings.get('ignored-scopes') 345 | highlight_scope = settings.get('highlight-scope') 346 | 347 | selection = self.view.sel()[0] # first selection (ignore rest) 348 | everything = sublime.Region(0, self.view.size()) 349 | check_region = everything if selection.empty() else selection 350 | check_text = self.view.substr(check_region) 351 | 352 | self.view.run_command("clear_language_problems") 353 | 354 | language = self.view.settings().get('language_tool_language', 'auto') 355 | ignored_ids = [rule['id'] for rule in load_ignored_rules()] 356 | 357 | matches = LTServer.getResponse(server_url, check_text, language, 358 | ignored_ids) 359 | 360 | if matches == None: 361 | set_status_bar('could not parse server response (may be due to' 362 | ' quota if using https://languagetool.org)') 363 | return 364 | 365 | def get_region(problem): 366 | """Return a Region object corresponding to problem text.""" 367 | length = problem['length'] 368 | offset = problem['offset'] 369 | return sublime.Region(offset, offset + length) 370 | 371 | def inside(problem): 372 | """Return True iff problem text is inside check_region.""" 373 | region = get_region(problem) 374 | return check_region.contains(region) 375 | 376 | def is_ignored(problem): 377 | """Return True iff any problem scope is ignored.""" 378 | scope_string = self.view.scope_name(problem['offset']) 379 | scopes = scope_string.split() 380 | return cross_match(scopes, ignored_scopes, fnmatch.fnmatch) 381 | 382 | def add_highlight_region(region_key, problem): 383 | region = get_region(problem) 384 | problem['orgContent'] = self.view.substr(region) 385 | problem['regionKey'] = region_key 386 | self.view.add_regions(region_key, [region], highlight_scope, "", 387 | sublime.DRAW_OUTLINED) 388 | 389 | 390 | shifter = lambda problem: shift_offset(problem, check_region.a) 391 | 392 | get_problem = compose(shifter, parse_match) 393 | 394 | problems = [problem for problem in map(get_problem, matches) 395 | if inside(problem) and not is_ignored(problem)] 396 | 397 | for index, problem in enumerate(problems): 398 | add_highlight_region(str(index), problem) 399 | 400 | if problems: 401 | select_problem(self.view, problems[0]) 402 | else: 403 | set_status_bar("no language problems were found :-)") 404 | 405 | self.view.problems = problems 406 | 407 | 408 | def compose(f1, f2): 409 | """Compose two functions.""" 410 | def inner(*args, **kwargs): 411 | return f1(f2(*args, **kwargs)) 412 | return inner 413 | 414 | 415 | def cross_match(list1, list2, predicate): 416 | """Cross match items from two lists using a predicate. 417 | 418 | Args: 419 | list1 (list): list 1. 420 | list2 (list): list 2. 421 | 422 | Returns: 423 | True iff predicate(x, y) is True for any x in list1 and y in list2, 424 | False otherwise. 425 | 426 | """ 427 | return any(predicate(x, y) for x, y in itertools.product(list1, list2)) 428 | 429 | 430 | def shift_offset(problem, shift): 431 | """Shift problem offset by `shift`.""" 432 | 433 | problem['offset'] += shift 434 | return problem 435 | 436 | 437 | def parse_match(match): 438 | """Parse a match object. 439 | 440 | Args: 441 | match (dict): match object returned by LanguageTool Server. 442 | 443 | Returns: 444 | dict: problem object. 445 | 446 | """ 447 | 448 | problem = { 449 | 'category': match['rule']['category']['name'], 450 | 'message': match['message'], 451 | 'replacements': [r['value'] for r in match['replacements']], 452 | 'rule': match['rule']['id'], 453 | 'urls': [w['value'] for w in match['rule'].get('urls', [])], 454 | 'offset': match['offset'], 455 | 'length': match['length'] 456 | } 457 | 458 | return problem 459 | 460 | 461 | class DeactivateRuleCommand(sublime_plugin.TextCommand): 462 | def run(self, edit): 463 | ignored = load_ignored_rules() 464 | v = self.view 465 | problems = v.__dict__.get("problems", []) 466 | sel = v.sel()[0] 467 | selected = [ 468 | p for p in problems 469 | if sel.contains(v.get_regions(p['regionKey'])[0]) 470 | ] 471 | if not selected: 472 | set_status_bar('select a problem to deactivate its rule') 473 | elif len(selected) == 1: 474 | rule = { 475 | "id": selected[0]['rule'], 476 | "description": selected[0]['message'] 477 | } 478 | ignored.append(rule) 479 | ignoredProblems = [p for p in problems if p['rule'] == rule['id']] 480 | for p in ignoredProblems: 481 | ignore_problem(p, v, edit) 482 | problems = [p for p in problems if p['rule'] != rule['id']] 483 | v.run_command("goto_next_language_problem") 484 | save_ignored_rules(ignored) 485 | set_status_bar('deactivated rule %s' % rule) 486 | else: 487 | set_status_bar('there are multiple selected problems;' 488 | ' select only one to deactivate') 489 | 490 | 491 | class ActivateRuleCommand(sublime_plugin.TextCommand): 492 | def run(self, edit): 493 | ignored = load_ignored_rules() 494 | if ignored: 495 | activate_callback_wrapper = lambda i: self.activate_callback(i) 496 | ruleList = [[rule['id'], rule['description']] for rule in ignored] 497 | self.view.window().show_quick_panel(ruleList, 498 | activate_callback_wrapper) 499 | else: 500 | set_status_bar('there are no ignored rules') 501 | 502 | def activate_callback(self, i): 503 | ignored = load_ignored_rules() 504 | if i != -1: 505 | activate_rule = ignored[i] 506 | ignored.remove(activate_rule) 507 | save_ignored_rules(ignored) 508 | set_status_bar('activated rule %s' % activate_rule['id']) 509 | 510 | 511 | class LanguageToolListener(sublime_plugin.EventListener): 512 | def on_modified(self, view): 513 | # buffer text was changed, recompute region highlights 514 | recompute_highlights(view) 515 | 516 | 517 | def recompute_highlights(view): 518 | problems = view.__dict__.get("problems", {}) 519 | hscope = get_settings().get("highlight-scope", "comment") 520 | for p in problems: 521 | rL = view.get_regions(p['regionKey']) 522 | if rL: 523 | regionScope = "" if is_problem_solved(view, p) else hscope 524 | view.add_regions(p['regionKey'], rL, regionScope, "", 525 | sublime.DRAW_OUTLINED) 526 | --------------------------------------------------------------------------------