|
6 | 6 | original_window: the regular window
|
7 | 7 | preview_window: the window with the markdown file and the preview
|
8 | 8 | """
|
9 |
| - |
10 |
| -import time |
11 | 9 | import os.path
|
12 |
| -import struct |
| 10 | +import time |
| 11 | +from functools import partial |
| 12 | + |
13 | 13 | import sublime
|
14 | 14 | import sublime_plugin
|
15 | 15 |
|
16 |
| -from functools import partial |
17 |
| - |
18 | 16 | from .markdown2html import markdown2html
|
19 | 17 |
|
20 |
| -MARKDOWN_VIEW_INFOS = "markdown_view_infos" |
21 |
| -PREVIEW_VIEW_INFOS = "preview_view_infos" |
22 |
| -SETTING_DELAY_BETWEEN_UPDATES = "delay_between_updates" |
23 |
| - |
| 18 | +PREVIEW_VIEW_INFO = "preview_view_info" |
24 | 19 | resources = {}
|
25 | 20 |
|
26 | 21 |
|
27 |
| -def plugin_loaded(): |
28 |
| - global DELAY |
29 |
| - resources["base64_404_image"] = parse_image_resource(get_resource("404.base64")) |
30 |
| - resources["base64_loading_image"] = parse_image_resource( |
31 |
| - get_resource("loading.base64") |
32 |
| - ) |
33 |
| - resources["stylesheet"] = get_resource("stylesheet.css") |
34 |
| - # FIXME: how could we make this setting update without restarting sublime text |
35 |
| - # and not loading it every update as well |
36 |
| - DELAY = get_settings().get(SETTING_DELAY_BETWEEN_UPDATES) |
37 |
| - |
38 |
| - |
39 |
| -class MdlpInsertCommand(sublime_plugin.TextCommand): |
40 |
| - def run(self, edit, point, string): |
41 |
| - self.view.insert(edit, point, string) |
42 |
| - |
43 |
| - |
44 |
| -class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand): |
45 |
| - def run(self, edit): |
46 |
| - |
47 |
| - """ If the file is saved exists on disk, we close it, and reopen it in a new |
48 |
| - window. Otherwise, we copy the content, erase it all (to close the file without |
49 |
| - a dialog) and re-insert it into a new view into a new window """ |
50 |
| - |
51 |
| - original_view = self.view |
52 |
| - original_window_id = original_view.window().id() |
53 |
| - file_name = original_view.file_name() |
54 |
| - |
55 |
| - syntax_file = original_view.settings().get("syntax") |
56 |
| - |
57 |
| - if file_name: |
58 |
| - original_view.close() |
59 |
| - else: |
60 |
| - # the file isn't saved, we need to restore the content manually |
61 |
| - total_region = sublime.Region(0, original_view.size()) |
62 |
| - content = original_view.substr(total_region) |
63 |
| - original_view.erase(edit, total_region) |
64 |
| - original_view.close() |
65 |
| - # FIXME: save the document to a temporary file, so that if we crash, |
66 |
| - # the user doesn't lose what he wrote |
67 |
| - |
68 |
| - sublime.run_command("new_window") |
69 |
| - preview_window = sublime.active_window() |
70 |
| - |
71 |
| - preview_window.run_command( |
72 |
| - "set_layout", |
73 |
| - { |
74 |
| - "cols": [0.0, 0.5, 1.0], |
75 |
| - "rows": [0.0, 1.0], |
76 |
| - "cells": [[0, 0, 1, 1], [1, 0, 2, 1]], |
77 |
| - }, |
78 |
| - ) |
79 |
| - |
80 |
| - preview_window.focus_group(1) |
81 |
| - preview_view = preview_window.new_file() |
82 |
| - preview_view.set_scratch(True) |
83 |
| - preview_view.settings().set(PREVIEW_VIEW_INFOS, {}) |
84 |
| - preview_view.set_name("Preview") |
85 |
| - # FIXME: hide number lines on preview |
86 |
| - |
87 |
| - preview_window.focus_group(0) |
88 |
| - if file_name: |
89 |
| - markdown_view = preview_window.open_file(file_name) |
90 |
| - else: |
91 |
| - markdown_view = preview_window.new_file() |
92 |
| - markdown_view.run_command("mdlp_insert", {"point": 0, "string": content}) |
93 |
| - markdown_view.set_scratch(True) |
| 22 | +def find_preview(view): |
| 23 | + """find previews for input view.""" |
| 24 | + view_id = view.id() |
| 25 | + for x in view.window().views(): |
| 26 | + d = x.settings().get(PREVIEW_VIEW_INFO) |
| 27 | + if d and d.get("id") == view_id: |
| 28 | + yield x |
94 | 29 |
|
95 |
| - markdown_view.set_syntax_file(syntax_file) |
96 |
| - markdown_view.settings().set( |
97 |
| - MARKDOWN_VIEW_INFOS, {"original_window_id": original_window_id,}, |
98 |
| - ) |
99 | 30 |
|
100 |
| - def is_enabled(self): |
101 |
| - # FIXME: is this the best way there is to check if the current syntax is markdown? |
102 |
| - # should we only support default markdown? |
103 |
| - # what about "md"? |
104 |
| - # FIXME: what about other languages, where markdown preview roughly works? |
105 |
| - return "markdown" in self.view.settings().get("syntax").lower() |
| 31 | +def get_resource(resource): |
| 32 | + path = "Packages/MarkdownLivePreview/resources/" + resource |
| 33 | + abs_path = os.path.join(sublime.packages_path(), "..", path) |
| 34 | + if os.path.isfile(abs_path): |
| 35 | + with open(abs_path, "r") as fp: |
| 36 | + return fp.read() |
| 37 | + return sublime.load_resource(path) |
106 | 38 |
|
107 | 39 |
|
108 | 40 | class MarkdownLivePreviewListener(sublime_plugin.EventListener):
|
109 |
| - |
110 |
| - phantom_sets = { |
111 |
| - # markdown_view.id(): phantom set |
112 |
| - } |
113 |
| - |
114 |
| - # we schedule an update for every key stroke, with a delay of DELAY |
115 |
| - # then, we update only if now() - last_update > DELAY |
116 |
| - last_update = 0 |
117 |
| - |
118 |
| - # FIXME: maybe we shouldn't restore the file in the original window... |
119 |
| - |
120 |
| - def on_pre_close(self, markdown_view): |
121 |
| - """ Close the view in the preview window, and store information for the on_close |
122 |
| - listener (see doc there) |
123 |
| - """ |
124 |
| - if not markdown_view.settings().get(MARKDOWN_VIEW_INFOS): |
125 |
| - return |
126 |
| - |
127 |
| - self.markdown_view = markdown_view |
128 |
| - self.preview_window = markdown_view.window() |
129 |
| - self.file_name = markdown_view.file_name() |
130 |
| - |
131 |
| - if self.file_name is None: |
132 |
| - total_region = sublime.Region(0, markdown_view.size()) |
133 |
| - self.content = markdown_view.substr(total_region) |
134 |
| - markdown_view.erase(edit, total_region) |
| 41 | + last_update = 0 # update only if now() - last_update > DELAY |
| 42 | + phantom_sets = {} # {preview.id(): PhantomSet} |
| 43 | + |
| 44 | + def on_pre_close(self, view): |
| 45 | + """Closing markdown files closes any associated previews.""" |
| 46 | + if "markdown" in view.settings().get("syntax").lower(): |
| 47 | + previews = list(find_preview(view)) |
| 48 | + if previews: |
| 49 | + window = view.window() |
| 50 | + for preview in previews: |
| 51 | + window.focus_view(preview) |
| 52 | + window.run_command("close_file") |
135 | 53 | else:
|
136 |
| - self.content = None |
137 |
| - |
138 |
| - def on_load_async(self, markdown_view): |
139 |
| - infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS) |
140 |
| - if not infos: |
141 |
| - return |
142 |
| - |
143 |
| - preview_view = markdown_view.window().active_view_in_group(1) |
144 |
| - |
145 |
| - self.phantom_sets[markdown_view.id()] = sublime.PhantomSet(preview_view) |
146 |
| - self._update_preview(markdown_view) |
147 |
| - |
148 |
| - def on_close(self, markdown_view): |
149 |
| - """ Use the information saved to restore the markdown_view as an original_view |
150 |
| - """ |
151 |
| - infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS) |
152 |
| - if not infos: |
153 |
| - return |
154 |
| - |
155 |
| - assert ( |
156 |
| - markdown_view.id() == self.markdown_view.id() |
157 |
| - ), "pre_close view.id() != close view.id()" |
158 |
| - |
159 |
| - del self.phantom_sets[markdown_view.id()] |
160 |
| - |
161 |
| - self.preview_window.run_command("close_window") |
162 |
| - |
163 |
| - # find the window with the right id |
164 |
| - original_window = next( |
165 |
| - window |
166 |
| - for window in sublime.windows() |
167 |
| - if window.id() == infos["original_window_id"] |
168 |
| - ) |
169 |
| - if self.file_name: |
170 |
| - original_window.open_file(self.file_name) |
171 |
| - else: |
172 |
| - assert markdown_view.is_scratch(), ( |
173 |
| - "markdown view of an unsaved file should " "be a scratch" |
174 |
| - ) |
175 |
| - # note here that this is called original_view, because it's what semantically |
176 |
| - # makes sense, but this original_view.id() will be different than the one |
177 |
| - # that we closed first to reopen in the preview window |
178 |
| - # shouldn't cause any trouble though |
179 |
| - original_view = original_window.new_file() |
180 |
| - original_view.run_command( |
181 |
| - "mdlp_insert", {"point": 0, "string": self.content} |
182 |
| - ) |
183 |
| - |
184 |
| - original_view.set_syntax_file(markdown_view.settings().get("syntax")) |
185 |
| - |
186 |
| - # here, views are NOT treated independently, which is theoretically wrong |
187 |
| - # but in practice, you can only edit one markdown file at a time, so it doesn't really |
188 |
| - # matter. |
189 |
| - # @min_time_between_call(.5) |
190 |
| - def on_modified_async(self, markdown_view): |
191 |
| - |
192 |
| - infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS) |
193 |
| - if not infos: |
194 |
| - return |
195 |
| - |
196 |
| - # we schedule an update, which won't run if an |
197 |
| - sublime.set_timeout(partial(self._update_preview, markdown_view), DELAY) |
198 |
| - |
199 |
| - def _update_preview(self, markdown_view): |
200 |
| - # if the buffer id is 0, that means that the markdown_view has been closed |
201 |
| - # This check is needed since a this function is used as a callback for when images |
202 |
| - # are loaded from the internet (ie. it could finish loading *after* the user |
203 |
| - # closes the markdown_view) |
| 54 | + d = view.settings().get(PREVIEW_VIEW_INFO) |
| 55 | + if d: |
| 56 | + view_id = view.id() |
| 57 | + if view_id in self.phantom_sets: |
| 58 | + del self.phantom_sets[view_id] |
| 59 | + |
| 60 | + def on_modified_async(self, view): |
| 61 | + """Schedule an update when changing markdown files""" |
| 62 | + if "markdown" in view.settings().get("syntax").lower(): |
| 63 | + sublime.set_timeout(partial(self.update_preview, view), DELAY) |
| 64 | + |
| 65 | + def update_preview(self, view): |
| 66 | + # if the buffer id is 0, that means that the markdown_view has been |
| 67 | + # closed. This check is needed since a this function is used as a |
| 68 | + # callback for when images are loaded from the internet (ie. it could |
| 69 | + # finish loading *after* the user closes the markdown_view) |
204 | 70 | if time.time() - self.last_update < DELAY / 1000:
|
205 | 71 | return
|
206 |
| - |
207 |
| - if markdown_view.buffer_id() == 0: |
| 72 | + if view.buffer_id() == 0: |
| 73 | + return |
| 74 | + previews = list(find_preview(view)) |
| 75 | + if not previews: |
208 | 76 | return
|
209 |
| - |
210 | 77 | self.last_update = time.time()
|
| 78 | + for preview in previews: |
| 79 | + html = markdown2html( |
| 80 | + view.substr(sublime.Region(0, view.size())), |
| 81 | + os.path.dirname(view.file_name()), |
| 82 | + partial(self.update_preview, view), |
| 83 | + resources, |
| 84 | + preview.viewport_extent()[0], |
| 85 | + ) |
| 86 | + self.phantom_sets[preview.id()].update( |
| 87 | + [sublime.Phantom(sublime.Region(0), html, sublime.LAYOUT_BLOCK, lambda x: sublime.run_command("open_url", {"url": x}))] |
| 88 | + ) |
211 | 89 |
|
212 |
| - total_region = sublime.Region(0, markdown_view.size()) |
213 |
| - markdown = markdown_view.substr(total_region) |
214 |
| - |
215 |
| - preview_view = markdown_view.window().active_view_in_group(1) |
216 |
| - viewport_width = preview_view.viewport_extent()[0] |
217 |
| - |
218 |
| - basepath = os.path.dirname(markdown_view.file_name()) |
219 |
| - html = markdown2html( |
220 |
| - markdown, |
221 |
| - basepath, |
222 |
| - partial(self._update_preview, markdown_view), |
223 |
| - resources, |
224 |
| - viewport_width, |
225 |
| - ) |
226 |
| - |
227 |
| - self.phantom_sets[markdown_view.id()].update( |
228 |
| - [ |
229 |
| - sublime.Phantom( |
230 |
| - sublime.Region(0), |
231 |
| - html, |
232 |
| - sublime.LAYOUT_BLOCK, |
233 |
| - lambda href: sublime.run_command("open_url", {"url": href}), |
234 |
| - ) |
235 |
| - ] |
236 |
| - ) |
237 |
| - |
238 |
| - |
239 |
| -def get_settings(): |
240 |
| - return sublime.load_settings("MarkdownLivePreview.sublime-settings") |
241 | 90 |
|
| 91 | +class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand): |
| 92 | + def run(self, edit): |
| 93 | + """Set to multi-pane layout. Open markdown preview in another pane.""" |
| 94 | + window = sublime.active_window() |
| 95 | + if window.num_groups() < 2: |
| 96 | + window.set_layout({"cols": [0.0, 0.5, 1.0], "rows": [0.0, 1.0], "cells": [[0, 0, 1, 1], [1, 0, 2, 1]]}) |
| 97 | + window.focus_group(0 if window.active_group() else 1) |
| 98 | + view = window.new_file() |
| 99 | + view.set_scratch(True) |
| 100 | + view.set_name("Preview") |
| 101 | + view.settings().set(PREVIEW_VIEW_INFO, {"id": self.view.id()}) |
| 102 | + ps = MarkdownLivePreviewListener.phantom_sets |
| 103 | + ps[view.id()] = sublime.PhantomSet(view) |
| 104 | + MarkdownLivePreviewListener().update_preview(self.view) |
| 105 | + window.focus_view(self.view) |
242 | 106 |
|
243 |
| -def get_resource(resource): |
244 |
| - path = "Packages/MarkdownLivePreview/resources/" + resource |
245 |
| - abs_path = os.path.join(sublime.packages_path(), "..", path) |
246 |
| - if os.path.isfile(abs_path): |
247 |
| - with open(abs_path, "r") as fp: |
248 |
| - return fp.read() |
249 |
| - return sublime.load_resource(path) |
| 107 | + def is_enabled(self): |
| 108 | + return "markdown" in self.view.settings().get("syntax").lower() |
250 | 109 |
|
251 | 110 |
|
252 | 111 | def parse_image_resource(text):
|
253 | 112 | width, height, base64_image = text.splitlines()
|
254 | 113 | return base64_image, (int(width), int(height))
|
255 | 114 |
|
256 | 115 |
|
257 |
| -# try to reload the resources if we save this file |
258 |
| -try: |
259 |
| - plugin_loaded() |
260 |
| -except OSError: |
261 |
| - pass |
| 116 | +def plugin_loaded(): |
| 117 | + global DELAY |
| 118 | + DELAY = sublime.load_settings("MarkdownLivePreview.sublime-settings").get("delay_between_updates") |
| 119 | + resources["base64_404_image"] = parse_image_resource(get_resource("404.base64")) |
| 120 | + resources["base64_loading_image"] = parse_image_resource(get_resource("loading.base64")) |
| 121 | + resources["stylesheet"] = get_resource("stylesheet.css") |
0 commit comments