-
Notifications
You must be signed in to change notification settings - Fork 0
/
diction.py
369 lines (295 loc) · 13.3 KB
/
diction.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# -*- coding: utf-8 -*-
import os
import platform
import sublime
import sublime_plugin
import subprocess
import re
diction_word_regions = []
SUGGESTIONS_IN_VIEW = {} # error organized per view to display
if platform.system() == 'Darwin':
os.environ['PATH'] += os.pathsep + '/usr/local/bin' # add this for OSX homebrew diction executable
# TODO:
# * regex only once
def debug(msg):
''' debug util method '''
if not settings.debug:
return
print("[Diction] {0}".format(msg))
class DictionMatchObject(object):
''' object for a single diction suggestion '''
def __init__(self, lineno, conflicting_phrase='', suggestion='', surrounding_text='', surrounding_after=True):
self.lineno = lineno
self.conflicting_phrase = conflicting_phrase
self.suggestion = suggestion
self.surrounding_text = surrounding_text
self.surrounding_after = surrounding_after
def __str__(self):
return 'lineno: ' + self.lineno + '\n' \
+ 'conflicting_phrase: ' + self.conflicting_phrase + '\n' \
+ 'suggestion: ' + self.suggestion + '\n' \
+ 'surrounding_after: ' + str(self.surrounding_after) + ' surrounding_text: ' \
+ self.surrounding_text + '\n'
def mark_words(view, search_all=True):
''' run the external diction executable, parse output, mark in editor and create the tooltip texts '''
global settings, diction_word_regions
window = sublime.active_window()
if window:
view = window.active_view()
def neighborhood(iterable):
''' generator function providing next and previous items for tokens '''
iterator = iter(iterable)
prev_token = None
item = next(iterator) # throws StopIteration if empty.
for next_token in iterator:
yield (prev_token, item, next_token)
prev_token = item
item = next_token
yield (prev_token, item, None)
def run_diction():
''' runs the diction executable and parses its output '''
diction_words = []
if view and view.file_name():
debug('\n\nrunning diction on file: ' + view.file_name())
try:
# add -s to get the suggestions from diction
output = subprocess.Popen([settings.diction_executable, '-qs', view.file_name()],
stdout=subprocess.PIPE).communicate()[0].decode('utf-8')
except OSError:
print('[Diction] Error. diction does not seem to be installed or is not in the PATH.')
prefiltered_output = output[:output.rfind('\n\n')]
# needed regexes
ex_brackets = re.compile('\[(.*?)\]')
ex_arrows_before = re.compile('(.*)(?= ->)')
ex_arrows_after = re.compile('-> (.*)')
for l in prefiltered_output.split('\n'):
if l.split(':') == ['']:
continue # empty lines of diction output
diction_text_for_line = ''.join(l.split(': ')[1:]) # strip the line no
# find the conflicting phrases in this line
prev_token = '' # in case there is no next token to align the text anymore (end of sentence, paragraph)
for prev_token, token, next_token in neighborhood(ex_brackets.split(diction_text_for_line)):
if '->' in token: # suggestion by diction: a new conflict found
new_diction_match_object = DictionMatchObject(l.split(': ')[0], diction_text_for_line, '', '')
new_diction_match_object.conflicting_phrase = ex_arrows_before.search(token).group()
new_diction_match_object.suggestion = ex_arrows_after.search(token).group()[3:]
if next_token is None or next_token.strip() == '':
# there is no next token. take the previous one
new_diction_match_object.surrounding_text = prev_token
new_diction_match_object.surrounding_after = False
else:
new_diction_match_object.surrounding_text = next_token
new_diction_match_object.surrounding_after = True
diction_words.append(new_diction_match_object)
debug('word tokens found:\n')
if settings.debug:
for nd in diction_words:
debug(nd)
SUGGESTIONS_IN_VIEW[view.id()] = diction_words
sublime.status_message(' Diction: ' + output[output.rfind('\n\n'):])
else:
print('buffer not saved to file. diction needs a file to work on. Abort')
return []
return diction_words
def find_words(words):
# construct the regex pattern for find_all
pattern = ''
found_regions = []
debug('searching whole document')
for w in words:
if w.surrounding_after:
pattern = re.escape(w.conflicting_phrase + w.surrounding_text)
else:
pattern = re.escape(w.surrounding_text + w.conflicting_phrase)
try:
# TODO: pattern non-greedy, use find with pos 0?
intermediate_regions = view.find_all(pattern, sublime.IGNORECASE, '', [])
# get line number of region of confl. phrase + pattern, as diction lists lines at beggining of sentence
# if different -> change in SUGGESTIONS_IN_VIEW[view.id()], so suggestions are on correct
# line number
if intermediate_regions:
if w.surrounding_after:
row, col = view.rowcol(intermediate_regions[0].a)
else:
row, col = view.rowcol(intermediate_regions[0].b)
w.lineno = row + 1
except UnicodeDecodeError:
continue # Skip on really weird input words
# to just mark the conflicting phrase and not the complete regex match, edit the regions >:)
for region in intermediate_regions:
found_regions.append(sublime.Region(region.a, region.a + len(w.conflicting_phrase)))
debug('Found regions: ' + str(found_regions))
return found_regions
def lazy_mark_regions(new_regions, old_regions, style_key, color_scope_name, symbol_name, draw_style):
if old_regions != new_regions or True:
view.erase_regions(style_key)
view.add_regions(style_key, new_regions, color_scope_name, symbol_name, draw_style)
return new_regions
# run diction, find the bad phrases
try:
out_flags = sublime.DRAW_NO_FILL + sublime.DRAW_NO_OUTLINE + sublime.DRAW_STIPPLED_UNDERLINE
except AttributeError: # nothing of this is available in ST2
out_flags = sublime.DRAW_OUTLINED
words = run_diction()
new_regions = find_words(words)
diction_word_regions = lazy_mark_regions(
new_regions,
diction_word_regions,
'Diction',
settings.color_scope_name,
'dot',
out_flags)
def clear_statusbar(view):
''' Clear status bar '''
view.erase_status('diction-tip')
def update_statusbar(view):
''' write suggestions to status bar '''
def get_current_line(view):
''' Get current line (line under cursor) '''
# get view selection (exit if no selection)
view_selection = view.sel()
if not view_selection:
return None
point = view_selection[0].end()
position = view.rowcol(point)
return position[0] + 1 # subl line starts at 0
# get diction view suggestions or return
view_sugs = SUGGESTIONS_IN_VIEW.get(view.id())
if view_sugs is None:
return
# get view selection (exit if no selection)
view_selection = view.sel()
if not view_selection:
return
current_line = get_current_line(view)
if current_line is None:
return
sugs_for_current_line = []
for e in view_sugs:
if current_line == int(e.lineno):
sugs_for_current_line.append(e)
if sugs_for_current_line: # there are suggestions for this line
view_str = ''
for sug in sugs_for_current_line:
view_str += sug.conflicting_phrase + ': ' + sug.suggestion + ' / '
debug('current_line: ' + str(current_line) + ' ' + view_str)
view.set_status('diction-tip', 'Diction: %s' % view_str)
else:
# no suggestions here, clear
view.erase_status('diction-tip')
class DictionCommand(sublime_plugin.TextCommand):
''' run gnu diction on current file '''
def run(self, edit):
debug('running diction command...')
window = sublime.active_window()
if window:
view = window.active_view()
if view:
mark_words(view, search_all=False)
class DictionDisableCommand(sublime_plugin.TextCommand):
''' disbale diction and erase highlighting '''
def run(self, edit):
DictionListener.disable()
class DictionListener(sublime_plugin.EventListener):
''' just triggers status bar update with cursor movement '''
enabled = False
def __init__(self, *args, **kwargs):
super(DictionListener, self).__init__(*args, **kwargs)
self._last_selected_line = None
@classmethod
def disable(cls):
''' disable package, remove marks on ui '''
cls.enabled = False
window = sublime.active_window()
if window:
view = window.active_view()
if view:
view.erase_regions("Diction")
def handle_event(self, view):
''' determines if the package status changed. marks words when turned on '''
global settings
settings = load_settings()
# does settings enable package?
if not settings.enabled:
DictionListener.disable()
return
# check this file if either it's enabled or if the extensions list is empty
file_name = view.file_name()
ext = ''
if file_name:
# Use the extension if it exists, otherwise use the whole filename
ext = os.path.splitext(file_name)
if ext[1]:
ext = ext[1]
else:
ext = os.path.split(file_name)[1]
allowed_extensions = settings.get('extensions')
if (not allowed_extensions) or ext in allowed_extensions:
if not DictionListener.enabled:
DictionListener.enabled = True
return
DictionListener.disable() # turn off for this file!
def on_activated(self, view):
if not view.is_loading():
self.handle_event(view)
def on_post_save(self, view):
self.handle_event(view)
def on_load(self, view):
self.handle_event(view)
def on_selection_modified(self, view):
''' cursor moved, check, if there is anything to display '''
if view.is_scratch(): # leave scratch views out
return
view_selection = view.sel()
if not view_selection:
return None
point = view_selection[0].end()
position = view.rowcol(point)
current_line = position[0]
if current_line is None:
if self._last_selected_line is not None: # line was selected
self._last_selected_line = None
view.erase_status('diction-tip')
elif current_line != self._last_selected_line: # line was changed
self._last_selected_line = current_line
debug('update statusbar.')
update_statusbar(view)
def load_settings():
''' process settings file on plugin reload '''
settings = sublime.load_settings('Diction.sublime-settings')
def process_settings(settings):
''' process settings from file '''
setattr(settings, 'enabled', settings.get('enabled', True))
setattr(settings, 'debug', settings.get('debug', True))
setattr(settings, 'color_scope_name', settings.get('color_scope_name', 'comment'))
setattr(settings, 'diction_executable', settings.get('diction_executable', 'diction'))
process_settings(settings)
if not settings.enabled:
DictionListener.disable()
# reload when package specific preferences changes
settings.add_on_change('reload', lambda: process_settings(settings))
return settings
settings = None
# only do this for ST2, use plugin_loaded for ST3.
if int(sublime.version()) < 3000:
settings = load_settings() # read settings as package loads.
def plugin_loaded():
'''
Seems that in ST3, plugins should read settings in this method.
See: http://www.sublimetext.com/forum/viewtopic.php?f=6&t=15160
'''
global settings
settings = load_settings() # read settings as package loads.
class ToggleDiction(sublime_plugin.ApplicationCommand):
''' menu item that toggles the enabled status of this package '''
def run(self):
global settings
settings.enabled = not settings.enabled
if not settings.enabled:
sublime.active_window().active_view().erase_regions("Diction")
else:
mark_words(sublime.active_window().active_view())
def description(self):
''' determines the text of the menu item '''
global settings
return 'Disable' if settings.enabled else 'Enable'