'
70 |
71 |
72 | class ErrorInfo(object):
73 | def __init__(self, data):
74 | self.data = data
75 |
76 | @property
77 | def start_line(self):
78 | return self.data['StartLine']
79 |
80 | @property
81 | def start_line_alternate(self):
82 | return self.data['StartLineAlternate']
83 |
84 | @property
85 | def end_line(self):
86 | return self.data['EndLine']
87 |
88 | @property
89 | def end_line_alternate(self):
90 | return self.data['EndLineAlternate']
91 |
92 | @property
93 | def start_column(self):
94 | return self.data['StartColumn']
95 |
96 | @property
97 | def end_column(self):
98 | return self.data['EndColumn']
99 |
100 | @property
101 | def length(self):
102 | return self.end_column - self.start_column
103 |
104 | @property
105 | def severity(self):
106 | return self.data['Severity']
107 |
108 | @property
109 | def message(self):
110 | return self.data['Message']
111 |
112 | @property
113 | def subcategory(self):
114 | return self.data['Subcategory']
115 |
116 | @property
117 | def file_name(self):
118 | return self.data['FileName']
119 |
120 | def to_region(self, view):
121 | row = self.start_line
122 | col = self.start_column
123 | pt = view.text_point(row, col)
124 | return sublime.Region(pt, pt + self.length)
125 |
126 | def to_regex_result_data(self):
127 | d = {
128 | 'file_name': self.file_name,
129 | 'severity': self.severity.upper(),
130 | 'subcategory': self.subcategory.upper(),
131 | 'start_line': self.start_line_alternate,
132 | 'start_column': int(self.start_column) + 1,
133 | 'message': self.message
134 | }
135 | return d
136 |
--------------------------------------------------------------------------------
/src/fsac/server.py:
--------------------------------------------------------------------------------
1 | import json
2 | import logging
3 | import os
4 | import queue
5 | import threading
6 |
7 | from FSharp.subtrees.plugin_lib.plat import is_windows
8 |
9 | from .pipe_server import PipeServer
10 |
11 |
12 | PATH_TO_FSAC = os.path.join(os.path.dirname(__file__),
13 | 'fsac/fsautocomplete.exe')
14 |
15 | # Incoming requests from client (plugin).
16 | requests_queue = queue.Queue()
17 | # Outgoing responses from FsacServer.
18 | responses_queue = queue.Queue()
19 | # Special response queue for completions.
20 | # Completions don't ever hit the regular `responses_queue`.
21 | completions_queue = queue.Queue()
22 | # Internal queue to orchestrate thread termination, etc.
23 | _internal_comm = queue.Queue()
24 |
25 | STOP_SIGNAL = '__STOP'
26 |
27 | _logger = logging.getLogger(__name__)
28 |
29 |
30 | def request_reader(requests, server, internal_msgs=_internal_comm):
31 | '''Reads requests from @requests and forwards them to @server.
32 |
33 | @requests
34 | A queue of requests.
35 | @server
36 | `FsacServer` instance.
37 | @internal_msgs
38 | A queue of internal messages for orchestration.
39 | '''
40 | while True:
41 | try:
42 | request = requests.get(block=True, timeout=5)
43 |
44 | try:
45 | # have we been asked to do anything?
46 | if internal_msgs.get(block=False) == STOP_SIGNAL:
47 | _logger.info('asked to exit; complying')
48 | internal_msgs.put(STOP_SIGNAL)
49 | break
50 | except queue.Empty:
51 | pass
52 | except Exception as e:
53 | _logger.error('unhandled exception: %s', e)
54 | # improve stack trace
55 | raise
56 |
57 | if not request:
58 | # Requests should always be valid, so log this but keep
59 | # running; most likely it isn't pathological.
60 | _logger.error('unexpected empty request: %s', request)
61 | continue
62 |
63 | _logger.debug('reading request: %s', request[:140])
64 | server.fsac.proc.stdin.write(request)
65 | server.fsac.proc.stdin.flush()
66 | except queue.Empty:
67 | continue
68 | except Exception as e:
69 | _logger.error('unhandled exception: %s', e)
70 | raise
71 |
72 | _logger.info("request reader exiting...")
73 |
74 |
75 | def response_reader(responses, server, internal_msgs=_internal_comm):
76 | '''Reads requests from @server and forwards them to @responses.
77 |
78 | @responses
79 | A queue of responses.
80 | @server
81 | `PipeServer` instance wrapping `fsautocomplete.exe`.
82 | @internal_msgs
83 | A queue of internal messages for orchestration.
84 | '''
85 | while True:
86 | try:
87 | data = server.fsac.proc.stdout.readline()
88 | if not data:
89 | _logger.debug('no data; exiting')
90 | break
91 |
92 | try:
93 | # Have we been asked to do anything?
94 | if internal_msgs.get(block=False) == STOP_SIGNAL:
95 | print('asked to exit; complying')
96 | internal_msgs.put(STOP_SIGNAL)
97 | break
98 | except queue.Empty:
99 | pass
100 | except Exception as e:
101 | _logger.error('unhandled exception: %s', e)
102 | raise
103 |
104 | _logger.debug('reading response: %s', data[:140])
105 | # TODO: if we're decoding here, .put() the decoded data.
106 | data_json = json.loads(data.decode('utf-8'))
107 | if data_json['Kind'] == 'completion':
108 | completions_queue.put(data)
109 | continue
110 |
111 | responses.put(data)
112 | except queue.Empty:
113 | continue
114 | except Exception as e:
115 | _logger.error('unhandled exception: %s', e)
116 | raise
117 |
118 | _logger.info("response reader exiting...")
119 |
120 |
121 | class FsacServer(object):
122 | '''Wraps `fsautocomplete.exe`.
123 | '''
124 | def __init__(self, cmd):
125 | '''
126 | @cmd
127 | A command line as a list to start fsautocomplete.exe.
128 | '''
129 | fsac = PipeServer(cmd)
130 | fsac.start()
131 | fsac.proc.stdin.write('outputmode json\n'.encode('ascii'))
132 | fsac.proc.stdin.flush()
133 | self.fsac = fsac
134 |
135 | threading.Thread(
136 | target=request_reader, args=(requests_queue, self)).start()
137 | threading.Thread(
138 | target=response_reader, args=(responses_queue, self)).start()
139 |
140 | def stop(self):
141 | self._internal_comm.put(STOP_SIGNAL)
142 | self.fsac.stop()
143 |
144 |
145 | def start(path=PATH_TO_FSAC):
146 | '''Starts a `FsacServer`.
147 |
148 | Returns a `PipeServer`.
149 |
150 | @path
151 | Path to `fsautocomplete.exe`.
152 | '''
153 | args = [path] if is_windows() else ['mono', path]
154 | return FsacServer(args)
155 |
--------------------------------------------------------------------------------
/src/fsharp.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details.
2 | # All rights reserved. Use of this source code is governed by a BSD-style
3 | # license that can be found in the LICENSE file.)
4 |
5 | import json
6 | import logging
7 | import os
8 | import queue
9 |
10 | import sublime
11 | import sublime_plugin
12 |
13 | from FSharp import PluginLogger
14 | from FSharp._init_ import editor_context
15 | from FSharp.fsac.request import AdHocRequest
16 | from FSharp.fsac.request import CompletionRequest
17 | from FSharp.fsac.request import DataRequest
18 | from FSharp.fsac.request import DeclarationsRequest
19 | from FSharp.fsac.request import FindDeclRequest
20 | from FSharp.fsac.request import ParseRequest
21 | from FSharp.fsac.request import ProjectRequest
22 | from FSharp.fsac.request import TooltipRequest
23 | from FSharp.fsac.response import CompilerLocationResponse
24 | from FSharp.fsac.response import DeclarationsResponse
25 | from FSharp.fsac.response import ErrorInfo
26 | from FSharp.fsac.response import ProjectResponse
27 | from FSharp.fsac.server import completions_queue
28 | from FSharp.lib.project import FileInfo
29 | from FSharp.lib.response_processor import add_listener
30 | from FSharp.lib.response_processor import ON_COMPLETIONS_REQUESTED
31 | from FSharp.lib.response_processor import process_resp
32 | from FSharp.lib.response_processor import raise_event
33 | from FSharp.subtrees.plugin_lib.context import ContextProviderMixin
34 | from FSharp.subtrees.plugin_lib.panels import OutputPanel
35 |
36 |
37 | _logger = PluginLogger(__name__)
38 |
39 |
40 | def erase_status(view, key):
41 | view.erase_status(key)
42 |
43 |
44 | class fs_run_fsac(sublime_plugin.WindowCommand):
45 | '''Runs an fsautocomplete.exe command.
46 | '''
47 | def run(self, cmd):
48 | _logger.debug ('running fsac action: %s', cmd)
49 | if not cmd:
50 | return
51 |
52 | if cmd == 'project':
53 | self.do_project()
54 | return
55 |
56 | if cmd == 'parse':
57 | self.do_parse()
58 | return
59 |
60 | if cmd == 'declarations':
61 | self.do_declarations()
62 | return
63 |
64 | if cmd == 'compilerlocation':
65 | self.do_compiler_location()
66 | return
67 |
68 | if cmd == 'finddecl':
69 | self.do_find_decl()
70 | return
71 |
72 | if cmd == 'completion':
73 | self.do_completion()
74 | return
75 |
76 | if cmd == 'tooltip':
77 | self.do_tooltip()
78 | return
79 |
80 | if cmd == 'run-file':
81 | self.do_run_file()
82 |
83 | def get_active_file_name(self):
84 | try:
85 | fname = self.window.active_view().file_name()
86 | except AttributeError as e:
87 | return
88 | return fname
89 |
90 | def get_insertion_point(self):
91 | view = self.window.active_view()
92 | if not view:
93 | return None
94 | try:
95 | sel = view.sel()[0]
96 | except IndexError as e:
97 | return None
98 | return view.rowcol(sel.b)
99 |
100 | def do_project(self):
101 | fname = self.get_active_file_name ()
102 | if not fname:
103 | return
104 | editor_context.fsac.send_request(ProjectRequest(fname))
105 |
106 | def do_parse(self):
107 | fname = self.get_active_file_name()
108 | if not fname:
109 | return
110 |
111 | try:
112 | v = self.window.active_view()
113 | except AttributeError:
114 | return
115 | else:
116 | if not v:
117 | return
118 | content = v.substr(sublime.Region(0, v.size()))
119 | editor_context.fsac.send_request(ParseRequest(fname, content=content))
120 |
121 | def do_declarations(self):
122 | fname = self.get_active_file_name()
123 | if not fname:
124 | return
125 | editor_context.fsac.send_request(DeclarationsRequest(fname))
126 |
127 | def do_compiler_location(self):
128 | editor_context.fsac.send_request(CompilerLocationRequest())
129 |
130 | def do_find_decl(self):
131 | fname = self.get_active_file_name()
132 | if not fname:
133 | return
134 |
135 | try:
136 | (row, col) = self.get_insertion_point()
137 | except TypeError as e:
138 | return
139 | else:
140 | self.do_parse()
141 | editor_context.fsac.send_request(FindDeclRequest(fname, row + 1, col + 1))
142 |
143 | def do_completion(self):
144 | fname = self.get_active_file_name ()
145 | if not fname:
146 | return
147 |
148 | try:
149 | (row, col) = self.get_insertion_point()
150 | except TypeError as e:
151 | return
152 | else:
153 | # raise first, because the event listener drains the completions queue
154 | raise_event(ON_COMPLETIONS_REQUESTED, {})
155 | self.do_parse()
156 | editor_context.fsac.send_request(CompletionRequest(fname, row + 1, col + 1))
157 | self.window.run_command('auto_complete')
158 |
159 | def do_tooltip(self):
160 | fname = self.get_active_file_name()
161 | if not fname:
162 | return
163 |
164 | try:
165 | (row, col) = self.get_insertion_point()
166 | except TypeError:
167 | return
168 | else:
169 | self.do_parse()
170 | editor_context.fsac.send_request(TooltipRequest(fname, row + 1, col + 1))
171 |
172 | def do_run_file(self):
173 | try:
174 | fname = self.window.active_view().file_name()
175 | except AttributeError:
176 | return
177 | else:
178 | self.window.run_command('fs_run_interpreter', {
179 | 'fname': fname
180 | })
181 |
182 |
183 | class fs_go_to_location (sublime_plugin.WindowCommand):
184 | def run(self, loc):
185 | v = self.window.active_view()
186 | pt = v.text_point(*loc)
187 | v.sel().clear()
188 | v.sel().add(sublime.Region(pt))
189 | v.show_at_center(pt)
190 |
191 |
192 | class fs_show_menu(sublime_plugin.WindowCommand):
193 | '''Generic command to show a menu.
194 | '''
195 | def run(self, items):
196 | '''
197 | @items
198 | A list of items following this structure:
199 | item 0: name
200 | item 1: Sublime Text command name
201 | item 2: dictionary of arguments for the command
202 | '''
203 | self.items = items
204 | self.names = names = [name for (name, _, _) in items]
205 | self.window.show_quick_panel(self.names, self.on_done)
206 |
207 | def on_done(self, idx):
208 | if idx == -1:
209 | return
210 | _, cmd, args = self.items[idx]
211 | if cmd:
212 | self.window.run_command (cmd, args or {})
213 |
214 |
215 | class fs_show_data(sublime_plugin.WindowCommand):
216 | '''A simple command to use the quick panel as a data display.
217 | '''
218 | def run(self, data):
219 | self.window.show_quick_panel(data, None, sublime.MONOSPACE_FONT)
220 |
221 |
222 | # TODO: move this to the command palette.
223 | class fs_show_options(sublime_plugin.WindowCommand):
224 | """Displays the main menu for F# commands.
225 | """
226 | ITEMS = {
227 | 'F#: Show Declarations': 'declarations',
228 | 'F#: Show Tooltip': 'tooltip',
229 | 'F#: Run File': 'run-file',
230 | }
231 |
232 | def run(self):
233 | self.window.show_quick_panel(
234 | list(sorted(fs_show_options.ITEMS.keys())),
235 | self.on_done)
236 |
237 | def on_done(self, idx):
238 | if idx == -1:
239 | return
240 | key = list(sorted(fs_show_options.ITEMS.keys()))[idx]
241 | cmd = fs_show_options.ITEMS[key]
242 | self.window.run_command('fs_run_fsac', {'cmd': cmd})
243 |
244 |
245 | class fs_run_interpreter(sublime_plugin.WindowCommand):
246 | def run(self, fname):
247 | assert fname, 'bad argument'
248 |
249 | f = FileInfo(fname)
250 | if not os.path.exists(f.path):
251 | _logger.debug('file must be saved first: %s', f.path)
252 | return
253 |
254 | if not f.is_fsharp_script_file:
255 | _logger.debug('not a script file: %s', f.path)
256 | return
257 |
258 | self.window.run_command('fs_exec', {
259 | 'shell_cmd': '"{}" "{}"'.format(editor_context.interpreter_path, f.path),
260 | 'working_dir': os.path.dirname(f.path)
261 | })
262 |
--------------------------------------------------------------------------------
/src/lib/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fsprojects/zarchive-sublime-fsharp-package/e8f29405b5ad1d452bc556ab4742fc9d9e84776b/src/lib/__init__.py
--------------------------------------------------------------------------------
/src/lib/editor.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details.
2 | # All rights reserved. Use of this source code is governed by a BSD-style
3 | # license that can be found in the LICENSE file.)
4 |
5 | import os
6 | import logging
7 | import threading
8 |
9 | import sublime
10 |
11 | from FSharp.fsac import server
12 | from FSharp.fsac.client import FsacClient
13 | from FSharp.fsac.request import CompilerLocationRequest
14 | from FSharp.fsac.request import ParseRequest
15 | from FSharp.fsac.request import ProjectRequest
16 | from FSharp.fsac.response import ErrorInfo
17 | from FSharp.lib import response_processor
18 | from FSharp.lib.errors_panel import FSharpErrorsPanel
19 | from FSharp.lib.project import FileInfo
20 | from FSharp.lib.project import FSharpProjectFile
21 | from FSharp.lib.response_processor import ON_COMPILER_PATH_AVAILABLE
22 | from FSharp.lib.response_processor import ON_ERRORS_AVAILABLE
23 |
24 |
25 | _logger = logging.getLogger(__name__)
26 |
27 |
28 | class Editor(object):
29 | """Global editor state.
30 | """
31 | def __init__(self, resp_proc):
32 | _logger.info ('Starting F# language services...')
33 |
34 | self.fsac = FsacClient(server.start(), resp_proc)
35 |
36 | self.compilers_path = None
37 | self.project_file = None
38 |
39 | self._errors = []
40 | self.errors_panel = FSharpErrorsPanel()
41 |
42 | self.fsac.send_request(CompilerLocationRequest())
43 | # todo: register as decorator instead?
44 | response_processor.add_listener(ON_COMPILER_PATH_AVAILABLE,
45 | self.on_compiler_path_available)
46 |
47 | response_processor.add_listener(ON_ERRORS_AVAILABLE,
48 | self.on_errors_available)
49 |
50 | self._write_lock = threading.Lock()
51 |
52 | def on_compiler_path_available(self, data):
53 | self.compilers_path = data['response'].compilers_path
54 |
55 | def on_errors_available(self, data):
56 | self.errors = data['response']['Data']
57 | self.errors_panel.update((ErrorInfo(e) for e in self.errors), sort_key=lambda x: x.start_line)
58 | self.errors_panel.display()
59 |
60 | @property
61 | def errors(self):
62 | with self._write_lock:
63 | return self._errors
64 |
65 | @errors.setter
66 | def errors(self, value):
67 | assert isinstance(value, list), 'bad call'
68 | with self._write_lock:
69 | self._errors = value
70 |
71 | @property
72 | def compiler_path(self):
73 | if self.compilers_path is None:
74 | return
75 | return os.path.join(self.compilers_path, 'fsc.exe')
76 |
77 | @property
78 | def interpreter_path(self):
79 | if self.compilers_path is None:
80 | return None
81 | return os.path.join(self.compilers_path, 'fsi.exe')
82 |
83 | def update_project_data(self, fs_file):
84 | assert isinstance(fs_file, FileInfo), 'wrong argument: %s' % fs_file
85 | # todo: run in alternate thread
86 |
87 | # fsautocomplete.exe doesn't link F# script files to any .fsproj file,
88 | # so bail out.
89 | if fs_file.is_fsharp_script_file:
90 | return
91 |
92 | if not self.project_file or not self.project_file.governs(fs_file.path):
93 | self.project_file = FSharpProjectFile.from_path(fs_file.path)
94 |
95 | if not self.project_file:
96 | _logger.info('could not find a .fsproj file for %s' % fs_file)
97 | return
98 |
99 | # fsautocomplete.exe takes care of managing .fsproj files, so we
100 | # can add as many as we need.
101 | self.set_project()
102 |
103 | def set_project(self):
104 | self.fsac.send_request(ProjectRequest(self.project_file.path))
105 |
106 | def parse_file(self, fs_file, content):
107 | self.fsac.send_request(ParseRequest(fs_file.path, content))
108 |
109 | def parse_view(self, view, force=False):
110 | """
111 | Sends a parse request to fsac.
112 |
113 | @view
114 | The view whose content should be parsed.
115 |
116 | @force
117 | If `True`, the @view will be parsed even if it's clean.
118 | """
119 |
120 | if not (view.is_dirty() or force):
121 | return
122 |
123 | # FIXME: In ST, I think a file may have a .file_name() and still not
124 | # exist on disk because it's been unlinked.
125 | # ignore unsaved files
126 | fs_file = FileInfo(view)
127 |
128 | self.update_project_data(fs_file)
129 | # TODO: very inneficient?
130 | if fs_file.is_fsharp_code:
131 | content = view.substr(sublime.Region(0, view.size()))
132 | self.parse_file(fs_file, content)
133 |
--------------------------------------------------------------------------------
/src/lib/errors_panel.py:
--------------------------------------------------------------------------------
1 | from FSharp.subtrees.plugin_lib.panels import ErrorsPanel
2 |
3 |
4 | class FSharpErrorsPanel(ErrorsPanel):
5 | """
6 | Customized error panel for FSharp.
7 | """
8 |
9 | def __init__(self):
10 | super().__init__(name='fsharp.errors')
11 |
12 | # Override defaults.
13 | self._sublime_syntax_file = 'Packages/FSharp/Support/FSharp Analyzer Output.sublime-syntax'
14 | self._tm_language_file = 'Packages/FSharp/Support/FSharp Analyzer Output.hidden-tmLanguage'
15 | self._errors_pattern = r'^\w+\|\w+\|(.+)\|(\d+)\|(\d+)\|(.+)$'
16 | self._errors_template = '{severity}|{subcategory}|{file_name}|{start_line}|{start_column}|{message}'
17 |
18 | def get_item_result_data(self, item):
19 | """
20 | Subclasses must implement this method.
21 |
22 | Must return a dictionary to be used as data for `errors_template`.
23 | """
24 | return item.to_regex_result_data()
25 |
--------------------------------------------------------------------------------
/src/lib/project.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details.
2 | # All rights reserved. Use of this source code is governed by a BSD-style
3 | # license that can be found in the LICENSE file.)
4 |
5 | import os
6 |
7 | from FSharp.subtrees.plugin_lib import path
8 | from FSharp.subtrees.plugin_lib.path import find_file_by_extension
9 |
10 |
11 | def find_fsproject(start):
12 | """
13 | Find a .fsproject file starting at @start path.
14 |
15 | Returns the path to the file or `None` if not found.
16 | """
17 |
18 | return find_file_by_extension(start, 'fsproj')
19 |
20 |
21 | class FileInfo(path.FileInfo):
22 | """
23 | Inspects a file for interesting properties from the plugin's POV.
24 | """
25 |
26 | def __init__(self, *args, **kwargs):
27 | super().__init__(*args, **kwargs)
28 |
29 | @property
30 | def is_fsharp_file(self):
31 | return any((self.is_fsharp_code_file,
32 | self.is_fsharp_script_file,
33 | self.is_fsharp_project_file))
34 |
35 | @property
36 | def is_fsharp_code(self):
37 | '''
38 | Returns `True` if `self` is any sort of F# code file.
39 | '''
40 | return (self.is_fsharp_code_file or self.is_fsharp_script_file)
41 |
42 | @property
43 | def is_fsharp_code_file(self):
44 | '''
45 | Returns `True` if `self` is a .fs file.
46 | '''
47 | return self.extension_equals('.fs')
48 |
49 | @property
50 | def is_fsharp_script_file(self):
51 | '''
52 | Returns `True` if `self` is a .fsx/.fsscript file.
53 | '''
54 | return self.extension_in('.fsx', '.fsscript')
55 |
56 | @property
57 | def is_fsharp_project_file(self):
58 | return self.extension_equals('.fsproj')
59 |
60 |
61 | class FSharpProjectFile(object):
62 | def __init__(self, path):
63 | assert path.endswith('.fsproj'), 'wrong fsproject path: %s' % path
64 | self.path = path
65 | self.parent = os.path.dirname(self.path)
66 |
67 | def __eq__(self, other):
68 | # todo: improve comparison
69 | return os.path.normpath(self.path) == os.path.normpath(other.path)
70 |
71 | def governs(self, fname):
72 | return fname.startswith(self.parent)
73 |
74 | @classmethod
75 | def from_path(cls, path):
76 | '''
77 | @path
78 | A path to a file or directory.
79 | '''
80 | fs_project = find_fsproject(path)
81 | if not fs_project:
82 | return None
83 | return FSharpProjectFile(fs_project)
84 |
--------------------------------------------------------------------------------
/src/lib/response_processor.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details.
2 | # All rights reserved. Use of this source code is governed by a BSD-style
3 | # license that can be found in the LICENSE file.)
4 |
5 | import json
6 | import logging
7 | import os
8 | import queue
9 |
10 | import sublime
11 | import sublime_plugin
12 |
13 | from FSharp.lib.tooltips import show_info_tooltip
14 | from FSharp.fsac.response import CompilerLocationResponse
15 | from FSharp.fsac.response import DeclarationsResponse
16 | from FSharp.fsac.response import ErrorInfo
17 | from FSharp.fsac.response import ProjectResponse
18 | from FSharp.subtrees.plugin_lib.panels import OutputPanel
19 |
20 |
21 | _logger = logging.getLogger(__name__)
22 |
23 |
24 | ON_COMPILER_PATH_AVAILABLE = 'OnCompilerPathAvailableEvent'
25 | ON_COMPLETIONS_REQUESTED = 'OnCompletionsRequestedEvent'
26 | ON_ERRORS_AVAILABLE = 'OnErrorsAvailableEvent'
27 |
28 | _events = {
29 | ON_COMPILER_PATH_AVAILABLE: [],
30 | ON_COMPLETIONS_REQUESTED: [],
31 | ON_ERRORS_AVAILABLE: [],
32 | }
33 |
34 |
35 | def add_listener(event_name, f):
36 | '''Registers a listener for the @event_name event.
37 | '''
38 | assert event_name, 'must provide "event_name" (actual: %s)' % event_name
39 | assert event_name in _events, 'unknown event name: %s' % event_name
40 | if f not in _events:
41 | _events[event_name].append(f)
42 |
43 |
44 | def raise_event(event_name=None, data={}):
45 | '''Raises an event.
46 | '''
47 | assert event_name, 'must provide "event_name" (actual: %s)' % event_name
48 | assert event_name in _events, 'unknown event name: %s' % event_name
49 | assert isinstance(data, dict), '`data` must be a dict'
50 | for f in _events[event_name]:
51 | f(data)
52 |
53 |
54 | def process_resp(data):
55 | _logger.debug ('processing response data: %s', data)
56 | if data ['Kind'] == 'compilerlocation':
57 | r = CompilerLocationResponse (data)
58 | raise_event(ON_COMPILER_PATH_AVAILABLE, {'response': r})
59 | return
60 |
61 | if data['Kind'] == 'project':
62 | r = ProjectResponse(data)
63 | _logger.debug('\n'.join(r.files))
64 | return
65 |
66 | if data['Kind'] == 'errors':
67 | # todo: enable error navigation via standard keys
68 | try:
69 | v = sublime.active_window().active_view()
70 | except AttributeError:
71 | return
72 |
73 | if not v:
74 | return
75 |
76 | v.erase_regions('fs.errs')
77 |
78 | raise_event(ON_ERRORS_AVAILABLE, {'response': data})
79 | if not data['Data']:
80 | return
81 |
82 | v.add_regions('fs.errs',
83 | [ErrorInfo(e).to_region(v) for e in data['Data']],
84 | 'invalid.illegal',
85 | 'dot',
86 | sublime.DRAW_SQUIGGLY_UNDERLINE |
87 | sublime.DRAW_NO_FILL |
88 | sublime.DRAW_NO_OUTLINE
89 | )
90 | return
91 |
92 | if data['Kind'] == 'ERROR':
93 | _logger.error(str(data))
94 | return
95 |
96 | if data['Kind'] == 'tooltip' and data['Data']:
97 | show_info_tooltip(data['Data'])
98 | return
99 |
100 | if data['Kind'] == 'INFO' and data['Data']:
101 | _logger.info(str(data))
102 | return
103 |
104 | if data['Kind'] == 'finddecl' and data['Data']:
105 | fname = data['Data']['File']
106 | row = data['Data']['Line']
107 | col = data['Data']['Column'] + 1
108 | w = sublime.active_window()
109 | # todo: don't open file if we are looking at the requested file
110 | target = '{0}:{1}:{2}'.format(fname, row, col)
111 | w.open_file(target, sublime.ENCODED_POSITION)
112 | return
113 |
114 | if data['Kind'] == 'declarations' and data['Data']:
115 | decls = DeclarationsResponse(data)
116 | its = [decl.to_menu_data() for decl in decls.declarations]
117 | w = sublime.active_window()
118 | w.run_command ('fs_show_menu', {'items': its})
119 | return
120 |
121 | if data['Kind'] == 'completion' and data['Data']:
122 | _logger.error('unexpected "completion" results - should be handled elsewhere')
123 | return
124 |
--------------------------------------------------------------------------------
/src/lib/tooltips.py:
--------------------------------------------------------------------------------
1 | import sublime
2 |
3 | from FSharp.subtrees.plugin_lib.sublime import after
4 |
5 |
6 | ERROR_TEMPLATE = """
7 |
13 |
14 | %(tag)s %(message)s
15 |
16 | """
17 |
18 | STATUS_TEMPLATE = """
19 |
22 |
23 | %s
24 |
25 | """
26 |
27 | TOOLTIP_ID = 0
28 |
29 |
30 | def next_id():
31 | global TOOLTIP_ID
32 | while True:
33 | TOOLTIP_ID += 1
34 | yield TOOLTIP_ID
35 | if TOOLTIP_ID > 100:
36 | TOOLTIP_ID = 0
37 |
38 |
39 | id_generator = next_id()
40 |
41 |
42 | def show_status_tooltip(content, view=None, location=-1, timeout=0):
43 | content = STATUS_TEMPLATE % content
44 | show_tooltip(content, view, location, timeout)
45 |
46 |
47 | def show_info_tooltip(content, view=None, location=-1, timeout=0):
48 | content = ERROR_TEMPLATE % {'severity': 'INFO', 'tag': 'I', 'message': content}
49 | show_tooltip(content, view, location, timeout)
50 |
51 |
52 | def show_analysis_tooltip(content, view=None, location=-1, timeout=0):
53 | content['tag'] = content['severity'][0]
54 | show_tooltip(ERROR_TEMPLATE % content, view, location, timeout)
55 |
56 |
57 | def show_tooltip(content, view=None, location=-1, timeout=0):
58 | '''
59 | Shows a tooltip.
60 |
61 | @content
62 | The tooltip's content (minihtml).
63 |
64 | @view
65 | The view in which the tooltip should be shown. If `None`, the active view
66 | will be used if available.
67 |
68 | @location
69 | Text location at which the tooltip will be shown.
70 |
71 | @timeout
72 | If greater than 0, the tooltip will be autohidden after @timeout
73 | milliseconds.
74 | '''
75 | if not view:
76 | try:
77 | view = sublime.active_window().active_view()
78 | except AttributeError as e:
79 | return
80 | else:
81 | if not view:
82 | return
83 |
84 | view.show_popup(content, location=location, max_width=500)
85 |
86 | if timeout > 0:
87 | current_id = next(id_generator)
88 | after(timeout, lambda: _hide(view, current_id))
89 |
90 |
91 | def _hide(view, target_tooltip_id):
92 | global TOOLTIP_ID
93 | if TOOLTIP_ID == target_tooltip_id:
94 | view.hide_popup()
95 |
--------------------------------------------------------------------------------
/src/subtrees/plugin_lib/LICENSE.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fsprojects/zarchive-sublime-fsharp-package/e8f29405b5ad1d452bc556ab4742fc9d9e84776b/src/subtrees/plugin_lib/LICENSE.txt
--------------------------------------------------------------------------------
/src/subtrees/plugin_lib/README.md:
--------------------------------------------------------------------------------
1 | plugin_lib
2 | ==========
3 |
4 | A library of tools to create Sublime Text plugins
5 |
--------------------------------------------------------------------------------
/src/subtrees/plugin_lib/__init__.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details.
2 | # All rights reserved. Use of this source code is governed by a BSD-style
3 | # license that can be found in the LICENSE file.)
4 |
5 | import logging
6 | import os
7 |
8 | import sublime
9 |
10 |
11 | class PluginLogger(object):
12 | """A logger intented to be used from plugin files inside this package.
13 | """
14 | def __init__(self, name):
15 | logger = logging.getLogger(name)
16 | logger.setLevel(logging.ERROR)
17 | self.logger = logger
18 |
19 | def debug(self, msg, *args, **kwargs):
20 | self.logger.debug(msg, *args, **kwargs)
21 |
22 | def info(self, msg, *args, **kwargs):
23 | self.logger.info(msg, *args, **kwargs)
24 |
25 | def warning(self, msg, *args, **kwargs):
26 | self.logger.warning(msg, *args, **kwargs)
27 |
28 | def error(self, msg, *args, **kwargs):
29 | self.logger.error(msg, *args, **kwargs)
30 |
31 | def critical(self, msg, *args, **kwargs):
32 | self.logger.critical(msg, *args, **kwargs)
33 |
--------------------------------------------------------------------------------
/src/subtrees/plugin_lib/collections.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details.
2 | # All rights reserved. Use of this source code is governed by a BSD-style
3 | # license that can be found in the LICENSE file.)
4 |
5 |
6 | class CircularArray(list):
7 | def __init__(self, *args, **kwargs):
8 | super().__init__(*args, **kwargs)
9 | self.index = None
10 |
11 | def forward(self):
12 | if self.index is None:
13 | self.index = 0
14 | return self[self.index]
15 |
16 | try:
17 | self.index += 1
18 | return self[self.index]
19 | except IndexError:
20 | self.index = 0
21 | return self[self.index]
22 |
23 | def backward(self):
24 | if self.index is None:
25 | self.index = -1
26 | return self[self.index]
27 |
28 | try:
29 | self.index -= 1
30 | return self[self.index]
31 | except IndexError:
32 | self.index = -1
33 | return self[self.index]
34 |
--------------------------------------------------------------------------------
/src/subtrees/plugin_lib/context.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details.
2 | # All rights reserved. Use of this source code is governed by a BSD-style
3 | # license that can be found in the LICENSE file.)
4 |
5 | import sublime
6 |
7 |
8 | class ContextProviderMixin(object):
9 | '''Provides a method to evaluate contexts.
10 |
11 | Useful with sublime_plugin.EventListeners that need to evaluate contexts.
12 | '''
13 | def _check(self, value, operator, operand, match_all):
14 | if operator == sublime.OP_EQUAL:
15 | if operand == True:
16 | return value
17 | elif operand == False:
18 | return not value
19 | elif operator == sublime.OP_NOT_EQUAL:
20 | if operand == True:
21 | return not value
22 | elif operand == False:
23 | return value
24 |
--------------------------------------------------------------------------------
/src/subtrees/plugin_lib/events.py:
--------------------------------------------------------------------------------
1 | import threading
2 | from collections import defaultdict
3 |
4 | import sublime_plugin
5 |
6 | from .sublime import after
7 |
8 |
9 | class IdleIntervalEventListener(sublime_plugin.EventListener):
10 | """
11 | Base class.
12 |
13 | Monitors view idle time and calls .on_idle() after the specified duration.
14 |
15 | Idle time is defined as time during which no calls to .on_modified[_async]()
16 | have been made.
17 |
18 | Subclasses must implement .on_idle(view) and, if necessary, .check(view).
19 |
20 | We don't provide a default implementation of .on_idle(view).
21 | """
22 |
23 | def __init__(self, *args, duration=500, **kwargs):
24 | """
25 | @duration
26 | Interval after which an .on_idle() call will be made, expressed in
27 | milliseconds.
28 | """
29 |
30 | # TODO: Maybe it's more efficient to collect .on_idle() functions and
31 | # manage edits globally, then call collected functions when idle.
32 | self.edits = defaultdict(int)
33 | self.lock = threading.Lock()
34 |
35 | # Expressed in milliseconds.
36 | self.duration = duration
37 | super().__init__(*args, **kwargs)
38 |
39 | @property
40 | def _is_subclass(self):
41 | return hasattr(self, 'on_idle')
42 |
43 | def _add_edit(self, view):
44 | with self.lock:
45 | self.edits[view.id()] += 1
46 | # TODO: are we running async or sync?
47 | after(self.duration, lambda: self._subtract_edit(view))
48 |
49 | def _subtract_edit(self, view):
50 | with self.lock:
51 | self.edits[view.id()] -= 1
52 | if self.edits[view.id()] == 0:
53 | self.on_idle(view)
54 |
55 | def on_modified_async(self, view):
56 | # TODO: improve check for widgets and overlays.
57 | if not all((view, self._is_subclass, self.check(view))):
58 | return
59 | self._add_edit(view)
60 |
61 | # Override in derived class if needed.
62 | def check(self, view):
63 | """
64 | Returs `True` if @view should be monitored for idleness.
65 |
66 | @view
67 | The view that is about to be monitored for idleness.
68 | """
69 | return True
70 |
--------------------------------------------------------------------------------
/src/subtrees/plugin_lib/filter.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details.
2 | # All rights reserved. Use of this source code is governed by a BSD-style
3 | # license that can be found in the LICENSE file.)
4 |
5 | from subprocess import Popen
6 | from subprocess import PIPE
7 | from subprocess import TimeoutExpired
8 | import threading
9 |
10 | from . import PluginLogger
11 | from .plat import supress_window
12 | from .text import clean
13 | from .text import decode
14 |
15 |
16 | _logger = PluginLogger(__name__)
17 |
18 |
19 | class TextFilter(object):
20 | '''Filters text through an external program (sync).
21 | '''
22 | def __init__(self, args, timeout=10):
23 | self.args = args
24 | self.timeout = timeout
25 | # Encoding the external program likes to receive.
26 | self.in_encoding = 'utf-8'
27 | # Encoding the external program will emit.
28 | self.out_encoding = 'utf-8'
29 |
30 | self._proc = None
31 |
32 | def encode(self, text):
33 | return text.encode(self.in_encoding)
34 |
35 | def _start(self):
36 | try:
37 | self._proc = Popen(self.args,
38 | stdout=PIPE,
39 | stderr=PIPE,
40 | stdin=PIPE,
41 | startupinfo=supress_window())
42 | except OSError as e:
43 | _logger.error('while starting text filter program: %s', e)
44 | return
45 |
46 | def filter(self, input_text):
47 | self._start()
48 | try:
49 | in_bytes = self.encode(input_text)
50 | out_bytes, err_bytes = self._proc.communicate(in_bytes,
51 | self.timeout)
52 | if err_bytes:
53 | _logger.error('while filtering text: %s',
54 | clean(decode(err_bytes, self.out_encoding)))
55 | return
56 |
57 | return clean(decode(out_bytes, self.out_encoding))
58 |
59 | except TimeoutExpired:
60 | _logger.debug('text filter program response timed out')
61 | return
62 |
63 | except Exception as e:
64 | _logger.error('while running TextFilter: %s', e)
65 | return
66 |
--------------------------------------------------------------------------------
/src/subtrees/plugin_lib/fs_completion.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details.
2 | # All rights reserved. Use of this source code is governed by a BSD-style
3 | # license that can be found in the LICENSE file.)
4 |
5 |
6 | # TODO(guillermooo): get fsystem items async
7 | # TODO(guillermooo): set limits to the no. of returned items
8 | # TODO(guillermooo): handle OS errors like permissions, etc.
9 | # TODO(guillermooo): performance: maybe store items in a tree,
10 | # skip list or some sort of indexed structure that improves recall time,
11 | # like indexing by prefix:
12 | # a, b, c, d, e, f, g ... ah, bh, ch, dh
13 |
14 |
15 | from collections import Counter
16 | import os
17 | import glob
18 |
19 |
20 | class CompletionsList(object):
21 | def __init__(self, items):
22 | self.items = items
23 |
24 | def __iter__(self):
25 | yield from self.items
26 |
27 | # TODO(guillermooo): move casesensitive to __init__
28 | def iter_prefixed(self, prefix, casesensitive=False):
29 | if casesensitive:
30 | yield from (item for item in self
31 | if item.startswith(prefix))
32 | else:
33 | yield from (item for item in self
34 | if item.lower().startswith(prefix.lower()))
35 |
36 |
37 | class FileSystemCompletion(object):
38 | def __init__(self, casesensitive=False):
39 | self.cached_items = None
40 | # path as provided by user
41 | self.user_path = None
42 | # TODO(guillermooo): set automatically based on OS
43 | self._casesensitive = casesensitive
44 |
45 | def do_refresh(self, new_path, force_refresh):
46 | seps_new = Counter(new_path)["/"]
47 | seps_old = Counter(self.user_path)["/"]
48 |
49 | # we've never tried to get completions yet, so try now
50 | if self.cached_items is None:
51 | self.user_path = os.path.abspath('.')
52 | return True
53 |
54 | # if we have 2 or more additional slashes, we can be sure the user
55 | # wants to drill down to a different directory.
56 | # if we had only 1 additional slash, it may indicate a directory, but
57 | # not necessarily any user-driven intention of drilling down in the
58 | # dir hierarchy. This is because we return items with slashes to
59 | # indicate directories.
60 | #
61 | # If we have fewer slashes in the new path, the user has modified it.
62 | if 0 > (seps_new - seps_old) > 1 or (seps_new - seps_old) < 0:
63 | return True
64 |
65 | return force_refresh
66 |
67 | def get_completions(self, path, force_refresh=False):
68 | # we are cycling through items in the same directory as last time,
69 | # so reuse the cached items
70 | if not self.do_refresh(path, force_refresh):
71 | cl = CompletionsList(self.cached_items)
72 | leaf = os.path.split(path)[1]
73 | return list(cl.iter_prefixed(
74 | leaf,
75 | casesensitive=self._casesensitive)
76 | )
77 |
78 | # we need to refresh the cache, as we are in a different directory
79 | # now or we've been asked to nevertheless.
80 | self.user_path = self.unescape(path)
81 | abs_path = os.path.abspath(os.path.dirname(self.user_path))
82 | leaf = os.path.split(self.user_path)[1]
83 |
84 | fs_items = glob.glob(self.user_path + '*')
85 | fs_items = self.process_items(fs_items)
86 |
87 | cl = CompletionsList(fs_items)
88 | self.cached_items = list(cl)
89 |
90 | return list(cl.iter_prefixed(leaf,
91 | casesensitive=self._casesensitive)
92 | )
93 |
94 | def process_items(self, items):
95 | processed = []
96 | for it in items:
97 | if not os.path.isdir(it):
98 | continue
99 | leaf = os.path.split(it)[1]
100 | leaf += '/'
101 | processed.append(self.escape(leaf))
102 | return processed
103 |
104 | @classmethod
105 | def escape(cls, name):
106 | return name.replace(' ', '\\ ')
107 |
108 | @classmethod
109 | def unescape(cls, name):
110 | return name.replace('\\ ', ' ')
111 |
--------------------------------------------------------------------------------
/src/subtrees/plugin_lib/io.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details.
2 | # All rights reserved. Use of this source code is governed by a BSD-style
3 | # license that can be found in the LICENSE file.)
4 |
5 | import threading
6 |
7 | import os
8 |
9 |
10 | class AsyncStreamReader(threading.Thread):
11 | '''Reads a process stream from an alternate thread.
12 | '''
13 | def __init__(self, stream, on_data, *args, **kwargs):
14 | '''
15 | @stream
16 | Stream to read from.
17 |
18 | @on_data
19 | Callback to call with bytes read from @stream.
20 | '''
21 | super().__init__(*args, **kwargs)
22 | self.stream = stream
23 | self.on_data = on_data
24 | assert self.on_data, 'wrong call: must provide callback'
25 |
26 | def run(self):
27 | while True:
28 | data = self.stream.readline()
29 | if not data:
30 | return
31 |
32 | self.on_data(data)
33 |
34 |
35 | def touch(path):
36 | with open(path, 'wb') as f:
37 | f.close()
38 |
--------------------------------------------------------------------------------
/src/subtrees/plugin_lib/panels.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details.
2 | # All rights reserved. Use of this source code is governed by a BSD-style
3 | # license that can be found in the LICENSE file.)
4 |
5 | from threading import Lock
6 | import os
7 |
8 | import sublime
9 |
10 | from .sublime import after
11 |
12 |
13 | class OutputPanel(object):
14 | _write_lock = Lock()
15 |
16 | """Manages an ST output panel.
17 |
18 | Can be used as a file-like object.
19 | """
20 |
21 | def __init__(self, name,
22 | base_dir=None,
23 | syntax='Packages/Text/Plain text.tmLanguage',
24 | **kwargs):
25 | """
26 | @name
27 | This panel's name.
28 | @base_dir
29 | Directory used to look files matched by regular expressions.
30 | @syntax:
31 | This panel's syntax.
32 | @kwargs
33 | Any number of settings to set in the underlying view via `.set()`.
34 |
35 | Common settings:
36 | - result_file_regex
37 | - result_line_regex
38 | - word_wrap
39 | - line_numbers
40 | - gutter
41 | - scroll_past_end
42 | """
43 |
44 | self.name = name
45 | self.window = sublime.active_window()
46 |
47 | if not hasattr(self, 'view'):
48 | # Try not to call get_output_panel until the regexes are assigned
49 | self.view = self.window.create_output_panel(self.name)
50 |
51 | # Default to the current file directory
52 | if (not base_dir and
53 | self.window.active_view() and
54 | self.window.active_view().file_name()):
55 | base_dir = os.path.dirname(self.window.active_view().file_name())
56 |
57 | self.set('result_base_dir', base_dir)
58 | self.set('syntax', syntax)
59 |
60 | self.set('result_file_regex', '')
61 | self.set('result_line_regex', '')
62 | self.set('word_wrap', False)
63 | self.set('line_numbers', False)
64 | self.set('gutter', False)
65 | self.set('scroll_past_end', False)
66 |
67 | def set(self, name, value):
68 | self.view.settings().set(name, value)
69 |
70 | def _clean_text(self, text):
71 | return text.replace('\r', '')
72 |
73 | def write(self, text):
74 | assert isinstance(text, str), 'must pass decoded text data'
75 | with OutputPanel._write_lock:
76 | do_write = lambda: self.view.run_command('append', {
77 | 'characters': self._clean_text(text),
78 | 'force': True,
79 | 'scroll_to_end': True,
80 | })
81 | # XXX: If we don't sync with the GUI thread here, the command above
82 | # won't work if this method is called from .set_timeout_async().
83 | # BUG?
84 | after(0, do_write)
85 |
86 | def flush(self):
87 | pass
88 |
89 | def hide(self):
90 | self.window.run_command('hide_panel', {
91 | 'panel': 'output.' + self.name})
92 |
93 | def show(self):
94 | # Call create_output_panel a second time after assigning the above
95 | # settings, so that it'll be picked up as a result buffer
96 | self.window.create_output_panel(self.name)
97 | self.window.run_command('show_panel', {
98 | 'panel': 'output.' + self.name})
99 |
100 | def close(self):
101 | pass
102 |
103 |
104 | # TOOD: fix this
105 | class ErrorPanel(object):
106 | def __init__(self):
107 | self.panel = OutputPanel('dart.info')
108 | self.panel.write('=' * 80)
109 | self.panel.write('\n')
110 | self.panel.write("Dart - Something's not quite right\n")
111 | self.panel.write('=' * 80)
112 | self.panel.write('\n')
113 | self.panel.write('\n')
114 |
115 | def write(self, text):
116 | self.panel.write(text)
117 |
118 | def show(self):
119 | self.panel.show()
120 |
121 |
122 | # TODO: move this to common plugin lib.
123 | class ErrorsPanel(object):
124 | """
125 | A panel that displays errors and enables error navigation.
126 | """
127 | _sublime_syntax_file = None
128 | _tm_language_file = None
129 | _errors_pattern = ''
130 | _errors_template = ''
131 |
132 | _lock = Lock()
133 |
134 | def __init__(self, name):
135 | """
136 | @name
137 | The name of the underlying output panel.
138 | """
139 | self.name = name
140 | self._errors = []
141 |
142 | @property
143 | def errors(self):
144 | with self._lock:
145 | return self._errors
146 |
147 | @errors.setter
148 | def errors(self, value):
149 | with self._lock:
150 | self._errors = value
151 |
152 | @property
153 | def errors_pattern(self):
154 | """
155 | Subclasses can override this to provide a more suitable pattern to
156 | capture errors.
157 | """
158 | return self._errors_pattern
159 |
160 | @property
161 | def errors_template(self):
162 | """
163 | Subclasses can override this to provide a more suitable template to
164 | display errors.
165 | """
166 | return self._errors_template
167 |
168 | def display(self):
169 | if len(self.errors) == 0:
170 | panel = OutputPanel(self.name)
171 | panel.hide()
172 | return
173 |
174 | # Like this to avoid deadlock. XXX: Maybe use RLock instead?
175 | formatted = self.format()
176 | with self._lock:
177 | # XXX: If we store this panel as an instance member, it won't work.
178 | # Revise implementation.
179 | panel = OutputPanel(self.name)
180 | panel.set('result_file_regex', self.errors_pattern)
181 | # TODO: remove this when we don't support tmLanguage any more.
182 | if sublime.version() > '3083':
183 | panel.view.set_syntax_file(self._sublime_syntax_file)
184 | else:
185 | panel.view.set_syntax_file(self._tm_language_file)
186 | panel.write(formatted)
187 | # TODO(guillermooo): Do not show now if other panel is showing;
188 | # for example, the console.
189 | panel.show()
190 |
191 | def clear(self):
192 | self.errors = []
193 |
194 | def update(self, errors, sort_key=None):
195 | self.errors = list(sorted(errors, key=sort_key))
196 |
197 | def get_item_result_data(self, item):
198 | """
199 | Subclasses must implement this method.
200 |
201 | Must return a dictionary to be used as data for `errors_template`.
202 | """
203 | return {}
204 |
205 | def format(self):
206 | formatted = (self.errors_template.format(**self.get_item_result_data(e))
207 | for e in self.errors)
208 | return '\n'.join(formatted)
209 |
--------------------------------------------------------------------------------
/src/subtrees/plugin_lib/path.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details.
2 | # All rights reserved. Use of this source code is governed by a BSD-style
3 | # license that can be found in the LICENSE file.)
4 |
5 | '''Helper functions for path management.
6 | '''
7 |
8 | import glob
9 | import os
10 | from os.path import join
11 | from contextlib import contextmanager
12 |
13 | import sublime
14 |
15 | from .plat import is_windows
16 |
17 |
18 | class FileInfo(object):
19 | """
20 | Base class.
21 |
22 | Subclasses inspect a file for interesting properties from a plugin's POV.
23 | """
24 |
25 | def __init__(self, view_or_fname):
26 | """
27 | @view_or_fname
28 | A Sublime Text view or a file name.
29 | """
30 | assert view_or_fname, 'wrong arg: %s' % view_or_fname
31 | self.view_or_fname = view_or_fname
32 |
33 | def __str__(self):
34 | return self.path
35 |
36 | @property
37 | def path(self):
38 | try:
39 | # The returned path can be None, for example, if the view is unsaved.
40 | return self.view_or_fname.file_name()
41 | except AttributeError:
42 | return self.view_or_fname
43 |
44 | def extension_equals(self, extension):
45 | return self.path and extension_equals(self.path, extension)
46 |
47 | def extension_in(self, *extensions):
48 | return self.path and any(self.extension_equals(ext) for ext in extensions)
49 |
50 |
51 | def extension_equals(path_or_view, extension):
52 | """Compares @path_or_view's extensions with @extension.
53 |
54 | Returns `True` if they are the same, `False` otherwise.
55 | Returns `False` if @path_or_view is a view and isn't saved on disk.
56 | """
57 | try:
58 | if path_or_view.file_name() is None:
59 | return False
60 | return extension_equals(path_or_view.file_name(), extension)
61 | except AttributeError:
62 | try:
63 | return os.path.splitext(path_or_view)[1] == extension
64 | except Exception:
65 | raise TypeError('string or view required, got {}'
66 | .format(type(path_or_view)))
67 |
68 |
69 | def find_in_path(name, win_ext=''):
70 | '''Searches PATH for @name.
71 |
72 | Returns the path containing @name or `None` if not found.
73 |
74 | @name
75 | Binary to search for.
76 |
77 | @win_ext
78 | An extension that will be added to @name on Windows.
79 | '''
80 | bin_name = join_on_win(name, win_ext)
81 | for path in os.environ['PATH'].split(os.path.pathsep):
82 | path = os.path.expandvars(os.path.expanduser(path))
83 | if os.path.exists(os.path.join(path, bin_name)):
84 | return os.path.realpath(path)
85 |
86 |
87 | def find_file_by_extension(start, extension):
88 | '''
89 | Finds a file in a directory hierarchy starting from @start and
90 | walking upwards.
91 |
92 | @start
93 | The directory to start from.
94 |
95 | @extension
96 | Sought extension.
97 | '''
98 | if not os.path.exists(start):
99 | return
100 |
101 | pattern = os.path.join(start, "*." + extension)
102 | file_name = glob.glob(pattern)
103 | if file_name:
104 | return file_name[0]
105 |
106 | if os.path.dirname(start) == start:
107 | return
108 |
109 | return find_file_by_extension(os.path.dirname(start), extension)
110 |
111 |
112 | def find_file(start, fname):
113 | '''Finds a file in a directory hierarchy starting from @start and
114 | walking backwards.
115 |
116 | @start
117 | The directory to start from.
118 |
119 | @fname
120 | Sought file.
121 | '''
122 | if not os.path.exists(start):
123 | return
124 |
125 | if os.path.exists(os.path.join(start, fname)):
126 | return os.path.join(start, fname)
127 |
128 | if os.path.dirname(start) == start:
129 | return
130 |
131 | return find_file(os.path.dirname(start), fname)
132 |
133 |
134 | def is_prefix(prefix, path):
135 | prefix = os.path.realpath(prefix)
136 | path = os.path.realpath(path)
137 | return path.startswith(prefix)
138 |
139 |
140 | def to_platform_path(original, append):
141 | """
142 | Useful to add .exe to @original, .bat, etc if ST is running on Windows.
143 |
144 | @original
145 | Original path.
146 | @append
147 | Fragment to append to @original on Windows.
148 | """
149 | if is_windows():
150 | if append.startswith('.'):
151 | return original + append
152 | return join(original, append)
153 | return original
154 |
155 |
156 | def is_active_path(path):
157 | """Returns `True` if the current view's path equals @path.
158 | """
159 | view = sublime.active_window().active_view()
160 | if not view:
161 | return
162 | return os.path.realpath(view.file_name()) == os.path.realpath(path)
163 |
164 |
165 | def is_active(view):
166 | """Returns `True` if @view is the view being currently edited.
167 | """
168 | active_view = sublime.active_window().active_view()
169 | if not active_view:
170 | return
171 | return active_view == view
172 |
173 |
174 | @contextmanager
175 | def pushd(to):
176 | old = os.getcwd()
177 | try:
178 | os.chdir(to)
179 | # TODO(guillermooo): makes more sense to return 'old'
180 | yield to
181 | finally:
182 | os.chdir(old)
183 |
184 |
185 | def join_on_win(original, append):
186 | """ Useful to add .exe, .bat, etc. to @original if ST is running on
187 | Windows.
188 |
189 | @original
190 | Original path.
191 |
192 | @append
193 | Fragment to append to @original on Windows. If it's an extension
194 | (the fragment begins with '.'), it's tucked at the end of @original.
195 | Otherwise, it's joined as a path.
196 | """
197 | if is_windows():
198 | if append.startswith('.'):
199 | return original + append
200 | return os.path.join(original, append)
201 | return original
202 |
--------------------------------------------------------------------------------
/src/subtrees/plugin_lib/plat.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details.
2 | # All rights reserved. Use of this source code is governed by a BSD-style
3 | # license that can be found in the LICENSE file.)
4 |
5 | '''Helper functions related to platform-specific issues.
6 | '''
7 |
8 | import sublime
9 |
10 | from os.path import join
11 | import subprocess
12 |
13 |
14 | def is_windows():
15 | """Returns `True` if ST is running on Windows.
16 | """
17 | return sublime.platform() == 'windows'
18 |
19 |
20 | def supress_window():
21 | """Returns a STARTUPINFO structure configured to supress windows.
22 | Useful, for example, to supress console windows.
23 |
24 | Works only on Windows.
25 | """
26 | if is_windows():
27 | startupinfo = subprocess.STARTUPINFO()
28 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
29 | startupinfo.wShowWindow = subprocess.SW_HIDE
30 | return startupinfo
31 | return None
32 |
--------------------------------------------------------------------------------
/src/subtrees/plugin_lib/settings.py:
--------------------------------------------------------------------------------
1 | import sublime
2 |
3 |
4 | # todo: move these tests to sublime_plugin_lib
5 | class FlexibleSetting(object):
6 | '''
7 | Base class.
8 |
9 | Data descriptor that encapsulates access to a Sublime Text setting that
10 | can take any of the following forms:
11 |
12 | Scalar value
13 | ======================================================================
14 | "color": "blue"
15 | ----------------------------------------------------------------------
16 | Dictionary keyed by platform
17 | ======================================================================
18 | "color" : {
19 | "windows": "blue",
20 | "linux": "orange",
21 | "osx": "green"
22 | }
23 | ----------------------------------------------------------------------
24 |
25 | This way, users can specify the same setting globally as a scalar value, or
26 | more granularly, by platform, and the plugin code can read it in the same
27 | way in both cases.
28 |
29 | For example:
30 |
31 | class SomeSettingsClass:
32 | path_to_thing = FlexibleSettingSubclass(name='path_to_thing')
33 |
34 | settings = SomeSettingsClass()
35 | value = settings.path_to_thing
36 |
37 | Optionally, basic validation is configurable:
38 |
39 | class SomeSettingsClass:
40 | path_to_thing = FlexibleSettingSubclass(name='path_to_thing', expected_type=str)
41 |
42 | settings = SomeSettingsClass()
43 | value = settings.path_to_thing
44 |
45 | Validation errors raise a ValueError.
46 |
47 | Subclasses must at a minimum implement the .get() method.
48 | '''
49 |
50 | def __init__(self, name, expected_type=None, default=None):
51 | self.name = name
52 | self.expected_type = expected_type
53 | self.default = default
54 |
55 | def __get__(self, obj, typ):
56 | if obj is None:
57 | return self
58 |
59 | scalar_or_dict = self.get(self.name)
60 |
61 | value = None
62 | try:
63 | value = scalar_or_dict[sublime.platform()]
64 | except TypeError:
65 | value = scalar_or_dict
66 | except KeyError:
67 | raise ValueError("no platform settings found for '%s' (%s)" % (self.name, sublime.platform()))
68 |
69 | value = value if (value is not None) else self.default
70 |
71 | value = self.validate(value)
72 | value = self.post_validate(value)
73 | return value
74 |
75 | def __set__(self, obj, val):
76 | raise NotImplementedException("can't do this now")
77 |
78 | def validate(self, value):
79 | if self.expected_type is None:
80 | return value
81 | assert isinstance(value, self.expected_type), 'validation failed for "%s". Got %s, expected %s' % (self.name, type(value), self.expected_type)
82 | return value
83 |
84 | def post_validate(self, value):
85 | '''
86 | Subclasses should override this method if they need to do any
87 | postprocessing after the the value has been gotten and validated.
88 |
89 | Returns a settings' value.
90 | '''
91 | return value
92 |
93 | def get(self, name):
94 | '''
95 | Abstract method.
96 |
97 | Subclasses must implement here access to self.name's setting top-level
98 | value (a scalar value or a dictionary).
99 | '''
100 | raise NotImplementedException('implement me')
101 |
--------------------------------------------------------------------------------
/src/subtrees/plugin_lib/sublime.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details.
2 | # All rights reserved. Use of this source code is governed by a BSD-style
3 | # license that can be found in the LICENSE file.)
4 |
5 | '''Utilities based on the Sublime Text api.
6 | '''
7 | import sublime
8 |
9 |
10 | # TODO(guillermooo): make an *_async version too?
11 | def after(timeout, f, *args, **kwargs):
12 | '''Runs @f after @timeout delay in milliseconds.
13 |
14 | @timeout
15 | Delay in milliseconds.
16 |
17 | @f
18 | Function to run passing it @*args and @*kwargs.
19 | '''
20 | sublime.set_timeout(lambda: f(*args, **kwargs), timeout)
21 |
--------------------------------------------------------------------------------
/src/subtrees/plugin_lib/subprocess.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details.
2 | # All rights reserved. Use of this source code is governed by a BSD-style
3 | # license that can be found in the LICENSE file.)
4 |
5 | from subprocess import Popen
6 | import os
7 |
8 | from . import PluginLogger
9 | from .plat import supress_window
10 |
11 |
12 | _logger = PluginLogger(__name__)
13 |
14 |
15 | def killwin32(proc):
16 | try:
17 | path = os.path.expandvars("%WINDIR%\\System32\\taskkill.exe")
18 | GenericBinary(show_window=False).start([path, "/pid", str(proc.pid)])
19 | except Exception as e:
20 | _logger.error(e)
21 |
22 |
23 | class GenericBinary(object):
24 | '''Starts a process.
25 | '''
26 | def __init__(self, *args, show_window=True):
27 | '''
28 | @show_window
29 | Windows only. Whether to show a window.
30 | '''
31 | self.args = args
32 | self.startupinfo = None
33 | if not show_window:
34 | self.startupinfo = supress_window()
35 |
36 | def start(self, args=[], env=None, shell=False, cwd=None):
37 | cmd = self.args + tuple(args)
38 | _logger.debug('running cmd line (GenericBinary): %s', cmd)
39 | Popen(cmd, startupinfo=self.startupinfo, env=env, shell=shell,
40 | cwd=cwd)
41 |
--------------------------------------------------------------------------------
/src/subtrees/plugin_lib/text.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details.
2 | # All rights reserved. Use of this source code is governed by a BSD-style
3 | # license that can be found in the LICENSE file.)
4 |
5 |
6 | def decode_and_clean(data_bytes, encoding='utf-8'):
7 | return clean(decode(data_bytes, encoding))
8 |
9 |
10 | def decode(data_bytes, encoding='utf-8'):
11 | return data_bytes.decode(encoding)
12 |
13 |
14 | def clean(text):
15 | return text.replace('\r', '')
16 |
--------------------------------------------------------------------------------
/src/test_runner.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details.
2 | # All rights reserved. Use of this source code is governed by a BSD-style
3 | # license that can be found in the LICENSE file.)
4 |
5 | import sublime
6 | import sublime_plugin
7 |
8 | import os
9 | import unittest
10 | import contextlib
11 | import threading
12 |
13 |
14 | from FSharp.subtrees.plugin_lib.panels import OutputPanel
15 |
16 |
17 | class RunFsharpTests(sublime_plugin.WindowCommand):
18 | '''Runs tests and displays the result.
19 |
20 | - Do not use ST while tests are running.
21 |
22 | @working_dir
23 | Required. Should be the parent of the top-level directory for `tests`.
24 |
25 | @loader_pattern
26 | Optional. Only run tests matching this glob.
27 |
28 | @active_file_only
29 | Optional. Only run tests in the active file in ST. Shadows
30 | @loader_pattern.
31 |
32 | To use this runner conveniently, open the command palette and select one
33 | of the `Build: Dart - Test *` commands.
34 | '''
35 | @contextlib.contextmanager
36 | def chdir(self, path=None):
37 | old_path = os.getcwd()
38 | if path is not None:
39 | assert os.path.exists(path), "'path' is invalid {}".format(path)
40 | os.chdir(path)
41 | yield
42 | if path is not None:
43 | os.chdir(old_path)
44 |
45 | def run(self, **kwargs):
46 | with self.chdir(kwargs.get('working_dir')):
47 | p = os.path.join(os.getcwd(), 'tests')
48 | patt = kwargs.get('loader_pattern', 'test*.py',)
49 | # TODO(guillermooo): I can't get $file to expand in the build
50 | # system. It should be possible to make the following code simpler
51 | # with it.
52 | if kwargs.get('active_file_only') is True:
53 | patt = os.path.basename(self.window.active_view().file_name())
54 | suite = unittest.TestLoader().discover(p, pattern=patt)
55 |
56 | file_regex = r'^\s*File\s*"([^.].*?)",\s*line\s*(\d+),.*$'
57 | display = OutputPanel('fs.tests', file_regex=file_regex)
58 | display.show()
59 | runner = unittest.TextTestRunner(stream=display, verbosity=1)
60 |
61 | def run_and_display():
62 | runner.run(suite)
63 |
64 | threading.Thread(target=run_and_display).start()
65 |
--------------------------------------------------------------------------------
/src/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fsprojects/zarchive-sublime-fsharp-package/e8f29405b5ad1d452bc556ab4742fc9d9e84776b/src/tests/__init__.py
--------------------------------------------------------------------------------
/src/tests/lib/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fsprojects/zarchive-sublime-fsharp-package/e8f29405b5ad1d452bc556ab4742fc9d9e84776b/src/tests/lib/__init__.py
--------------------------------------------------------------------------------
/src/tests/lib/test_editor.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/fsprojects/zarchive-sublime-fsharp-package/e8f29405b5ad1d452bc556ab4742fc9d9e84776b/src/tests/lib/test_editor.py
--------------------------------------------------------------------------------
/src/tests/lib/test_events.py:
--------------------------------------------------------------------------------
1 | import unittest
2 |
3 | from FSharp.subtrees.plugin_lib.events import IdleIntervalEventListener
4 |
5 |
6 | class Test_IdleIntervalEventListener(unittest.TestCase):
7 | def testDefaultIdleInterval(self):
8 | listener = IdleIntervalEventListener()
9 | self.assertEqual(500, listener.duration)
10 |
11 | def testDoesNotImplement_on_idle(self):
12 | listener = IdleIntervalEventListener()
13 | self.assertFalse(hasattr(listener, 'on_idle'))
14 |
15 | def test_check_ReturnsTrueByDefault(self):
16 | listener = IdleIntervalEventListener()
17 | self.assertTrue(listener.check(view=None))
18 |
--------------------------------------------------------------------------------
/src/tests/lib/test_project.py:
--------------------------------------------------------------------------------
1 | import contextlib
2 | import glob
3 | import os
4 | import tempfile
5 | import time
6 | import unittest
7 |
8 | import sublime
9 |
10 | from FSharp.lib.project import find_fsproject
11 | from FSharp.lib.project import FileInfo
12 | from FSharp.lib.project import FSharpProjectFile
13 | from FSharp.subtrees.plugin_lib.io import touch
14 |
15 |
16 | @contextlib.contextmanager
17 | def make_directories(dirs):
18 | tmp_dir = tempfile.TemporaryDirectory()
19 | current = tmp_dir.name
20 | for dd in dirs:
21 | for d in dd:
22 | current = os.path.join(current, d)
23 | os.mkdir(current)
24 | current = tmp_dir.name
25 | yield tmp_dir.name
26 | tmp_dir.cleanup()
27 |
28 |
29 | class Test_find_fsproject(unittest.TestCase):
30 | def testCanFind(self):
31 | with make_directories([["foo", "bar", "baz"]]) as tmp_root:
32 | fs_proj_file = os.path.join(tmp_root, 'hey.fsproj')
33 | touch(fs_proj_file)
34 | found = find_fsproject (os.path.join(tmp_root, 'foo/bar/baz'))
35 | self.assertEquals(found, fs_proj_file)
36 |
37 |
38 | class Test_FSharpProjectFile (unittest.TestCase):
39 | def testCanCreateFromPath(self):
40 | with tempfile.TemporaryDirectory() as tmp:
41 | f = os.path.join (tmp, 'foo.fsproj')
42 | touch (f)
43 | fs_project = FSharpProjectFile.from_path(f)
44 | self.assertEquals(fs_project.path, f)
45 |
46 | def testCanReturnParent(self):
47 | with tempfile.TemporaryDirectory() as tmp:
48 | f = os.path.join (tmp, 'foo.fsproj')
49 | touch (f)
50 | fs_project = FSharpProjectFile.from_path(f)
51 | self.assertEquals(fs_project.parent, tmp)
52 |
53 | def testCanBeCompared(self):
54 | with tempfile.TemporaryDirectory() as tmp:
55 | f = os.path.join (tmp, 'foo.fsproj')
56 | touch (f)
57 | fs_project_1 = FSharpProjectFile.from_path(f)
58 | fs_project_2 = FSharpProjectFile.from_path(f)
59 | self.assertEquals(fs_project_1, fs_project_2)
60 |
61 | def test_governs_SameLevel(self):
62 | with tempfile.TemporaryDirectory() as tmp:
63 | f = os.path.join (tmp, 'foo.fsproj')
64 | f2 = os.path.join (tmp, 'foo.fs')
65 | touch (f)
66 | touch (f2)
67 | fs_proj = FSharpProjectFile.from_path(f)
68 | self.assertTrue(fs_proj.governs (f2))
69 |
70 |
71 | class Test_FSharpFile(unittest.TestCase):
72 | def setUp(self):
73 | self.win = sublime.active_window()
74 |
75 | def tearDown(self):
76 | self.win.run_command('close')
77 |
78 | def testCannotDetectCodeFileBasedOnlyOnSyntaxDef(self):
79 | v = self.win.new_file()
80 | v.set_syntax_file('Packages/FSharp/FSharp.tmLanguage')
81 | file_info = FileInfo(v)
82 | self.assertFalse(file_info.is_fsharp_code)
83 |
84 | def testCanDetectCodeFile(self):
85 | with tempfile.TemporaryDirectory() as tmp:
86 | f = os.path.join (tmp, 'foo.fs')
87 | touch (f)
88 | v = self.win.open_file(f)
89 | time.sleep(0.01)
90 | file_info = FileInfo(v)
91 | self.assertTrue (file_info.is_fsharp_code_file)
92 |
93 | def testCanDetectScriptFile(self):
94 | with tempfile.TemporaryDirectory() as tmp:
95 | f = os.path.join (tmp, 'foo.fsx')
96 | touch (f)
97 | v = self.win.open_file(f)
98 | time.sleep(0.01)
99 | file_info = FileInfo(v)
100 | self.assertTrue (file_info.is_fsharp_script_file)
101 |
102 | def testCanDetectCodeForCodeFile(self):
103 | with tempfile.TemporaryDirectory() as tmp:
104 | f = os.path.join (tmp, 'foo.fs')
105 | touch (f)
106 | v = self.win.open_file(f)
107 | time.sleep(0.01)
108 | file_info = FileInfo(v)
109 | self.assertTrue (file_info.is_fsharp_code)
110 |
111 | def testCanDetectCodeForScriptFile(self):
112 | with tempfile.TemporaryDirectory() as tmp:
113 | f = os.path.join (tmp, 'foo.fsx')
114 | touch (f)
115 | v = self.win.open_file(f)
116 | time.sleep(0.01)
117 | file_info = FileInfo(v)
118 | self.assertTrue (file_info.is_fsharp_code)
119 |
120 | def testCanDetectProjectFile(self):
121 | with tempfile.TemporaryDirectory() as tmp:
122 | f = os.path.join (tmp, 'foo.fsproj')
123 | touch (f)
124 | v = self.win.open_file(f)
125 | time.sleep(0.01)
126 | file_info = FileInfo (v)
127 | self.assertTrue (file_info.is_fsharp_project_file)
128 |
129 |
130 | class Test_FSharpFile_path(unittest.TestCase):
131 | def setUp(self):
132 | self.win = sublime.active_window()
133 |
134 | def tearDown(self):
135 | self.win.run_command('close')
136 |
137 | def testCannotFindPathIfNone(self):
138 | v = self.win.new_file()
139 | v.set_syntax_file('Packages/FSharp/FSharp.tmLanguage')
140 | file_info = FileInfo(v)
141 | self.assertEquals(file_info.path, None)
142 |
--------------------------------------------------------------------------------
/src/xevents.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2014, Guillermo López-Anglada. Please see the AUTHORS file for details.
2 | # All rights reserved. Use of this source code is governed by a BSD-style
3 | # license that can be found in the LICENSE file.)
4 |
5 | from collections import defaultdict
6 | import json
7 | import logging
8 | import threading
9 |
10 | import sublime
11 | import sublime_plugin
12 |
13 | from FSharp import PluginLogger
14 | from FSharp.fsac.server import completions_queue
15 | from FSharp.fsharp import editor_context
16 | from FSharp.lib.project import FileInfo
17 | from FSharp.lib.response_processor import add_listener
18 | from FSharp.lib.response_processor import ON_COMPLETIONS_REQUESTED
19 | from FSharp.subtrees.plugin_lib.context import ContextProviderMixin
20 | from FSharp.subtrees.plugin_lib.sublime import after
21 | from FSharp.subtrees.plugin_lib.events import IdleIntervalEventListener
22 |
23 |
24 | _logger = PluginLogger(__name__)
25 |
26 |
27 | class IdleParser(IdleIntervalEventListener):
28 | """
29 | Reparses the current view after @self.duration milliseconds of inactivity.
30 | """
31 |
32 | def __init__(self, *args, **kwargs):
33 | super().__init__(*args, **kwargs)
34 | self.duration = 1000
35 |
36 | def check(self, view):
37 | return FileInfo(view).is_fsharp_code
38 |
39 | def on_idle(self, view):
40 | editor_context.parse_view(view)
41 |
42 |
43 | class IdleAutocomplete(IdleIntervalEventListener):
44 | """
45 | Shows the autocomplete list after @self.duration milliseconds of inactivity.
46 | """
47 |
48 | def __init__(self, *args, **kwargs):
49 | super().__init__(*args, **kwargs)
50 | self.duration = 400
51 |
52 | # FIXME: we should exclude widgets and overlays in the base class.
53 | def check(self, view):
54 | # Offer F# completions in F# files when the caret isn't in a string or
55 | # comment. If strings or comments, offer plain Sublime Text completions.
56 | return (not self._in_string_or_comment(view)
57 | and FileInfo(view).is_fsharp_code)
58 |
59 | def on_idle(self, view):
60 | self._show_completions(view)
61 |
62 | def _show_completions(self, view):
63 | try:
64 | # TODO: We probably should show completions after other chars.
65 | is_after_dot = view.substr(view.sel()[0].b - 1) == '.'
66 | except IndexError:
67 | return
68 |
69 | if is_after_dot:
70 | view.window().run_command('fs_run_fsac', {'cmd': 'completion'})
71 |
72 | def _in_string_or_comment(self, view):
73 | try:
74 | return view.match_selector(view.sel()[0].b,
75 | 'source.fsharp string, source.fsharp comment')
76 | except IndexError:
77 | pass
78 |
79 |
80 | class FSharpProjectTracker(sublime_plugin.EventListener):
81 | """
82 | Event listeners.
83 | """
84 |
85 | parsed = {}
86 | parsed_lock = threading.Lock()
87 |
88 | def on_activated_async(self, view):
89 | # It seems we may receive a None in some cases -- check for it.
90 | if not view or not view.file_name() or not FileInfo(view).is_fsharp_code:
91 | return
92 |
93 | with FSharpProjectTracker.parsed_lock:
94 | view_id = view.file_name() or view.id()
95 | if FSharpProjectTracker.parsed.get(view_id):
96 | return
97 |
98 | editor_context.parse_view(view, force=True)
99 | self.set_parsed(view, True)
100 |
101 | def on_load_async(self, view):
102 | self.on_activated_async(view)
103 |
104 | def set_parsed(self, view, value):
105 | with FSharpProjectTracker.parsed_lock:
106 | view_id = view.file_name() or view.id()
107 | FSharpProjectTracker.parsed[view_id] = value
108 |
109 | def on_modified_async(self, view):
110 | if not view or not view.file_name() or not FileInfo(view).is_fsharp_code:
111 | return
112 |
113 | self.set_parsed(view, False)
114 |
115 |
116 | class FSharpContextProvider(sublime_plugin.EventListener, ContextProviderMixin):
117 | """
118 | Implements contexts for .sublime-keymap files.
119 | """
120 |
121 | def on_query_context(self, view, key, operator, operand, match_all):
122 | if key == 'fs_is_code_file':
123 | value = FileInfo(view).is_fsharp_code
124 | return self._check(value, operator, operand, match_all)
125 |
126 |
127 | class FSharpAutocomplete(sublime_plugin.EventListener):
128 | """
129 | Provides completion suggestions from fsautocomplete.
130 | """
131 |
132 | WAIT_ON_COMPLETIONS = False
133 | _INHIBIT_OTHER = (sublime.INHIBIT_WORD_COMPLETIONS |
134 | sublime.INHIBIT_EXPLICIT_COMPLETIONS)
135 |
136 | @staticmethod
137 | def on_completions_requested(data):
138 | FSharpAutocomplete.WAIT_ON_COMPLETIONS = True
139 |
140 | @staticmethod
141 | def fetch_completions():
142 | data = completions_queue.get(block=True, timeout=.75)
143 | data = json.loads(data.decode('utf-8'))
144 | completions = [[item["Name"], item["Name"]] for item in data['Data']]
145 | return completions
146 |
147 | @staticmethod
148 | def _in_string_or_comment(view, locations):
149 | return all((view.match_selector(loc, 'source.fsharp comment, source.fsharp string')
150 | or view.match_selector(loc - 1, 'source.fsharp comment, sorce.fsharp string'))
151 | for loc in locations)
152 |
153 | def on_query_completions(self, view, prefix, locations):
154 | if not FSharpAutocomplete.WAIT_ON_COMPLETIONS:
155 | if not FileInfo(view).is_fsharp_code:
156 | return []
157 |
158 | if self._in_string_or_comment(view, locations):
159 | return []
160 |
161 | return ([], self._INHIBIT_OTHER)
162 |
163 | try:
164 | return (self.fetch_completions(), self._INHIBIT_OTHER)
165 | # FIXME: Be more explicit about caught exceptions.
166 | except:
167 | return ([], self._INHIBIT_OTHER)
168 | finally:
169 | FSharpAutocomplete.WAIT_ON_COMPLETIONS = False
170 |
171 |
172 | # TODO: make decorator?
173 | add_listener(ON_COMPLETIONS_REQUESTED, FSharpAutocomplete.on_completions_requested)
174 |
--------------------------------------------------------------------------------