-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWikiLinks.py
More file actions
319 lines (262 loc) · 13.4 KB
/
Copy pathWikiLinks.py
File metadata and controls
319 lines (262 loc) · 13.4 KB
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
import sublime
import sublime_plugin
import os
import re
class WikiLinkListener(sublime_plugin.EventListener):
WIKI_LINK_PATTERN = r'\[\[(.*?)\]\]'
def __init__(self): print("WikiLinks plugin loaded successfully")
def _startup_message(self): print("WikiLinks plugin is running")
def on_hover(self, view, point, hover_zone):
if hover_zone != sublime.HOVER_TEXT: return
link_region = self._get_link_at_cursor(view, point)
if not link_region: return
file_name, fragment = self._extract_file_name(view, link_region)
self._show_link_popup(view, point, file_name, fragment)
def on_text_command(self, view, command_name, args):
# Check if args is not None first, because it can be.
ctrl_pressed = args and args.get('additive') == True
if command_name == 'drag_select' and ctrl_pressed:
if len(view.sel()) > 0:
point = view.sel()[0].begin()
link_region = self._get_link_at_cursor(view, point)
if link_region:
file_name, fragment = self._extract_file_name(view, link_region)
print("WikiLinks: CTRL+Click detected on link '{}'".format(file_name))
self._navigate_to_file(view, file_name, fragment)
return ('wiki_link_navigate', {'file_name': file_name})
return None
def _get_link_at_cursor(self, view, point):
for region in view.find_all(self.WIKI_LINK_PATTERN):
if region.contains(point):
return region
return None
def _extract_file_name(self, view, region):
text = view.substr(region)
match = re.match(self.WIKI_LINK_PATTERN, text)
if match:
link_content = match.group(1)
# Check for fragment identifier (e.g., [[file#section]])
if '#' in link_content:
parts = link_content.split('#', 1)
return parts[0], parts[1]
return link_content, None
return text[2:-2], None
def _show_link_popup(self, view, point, file_name, fragment=None):
file_path = self._find_file_in_workspace(view, file_name)
if file_path and os.path.isfile(file_path):
# Get file preview content
preview_content = self._get_file_preview(file_path, fragment)
display_name = "{}#{}".format(os.path.basename(file_path), fragment) if fragment else os.path.basename(file_path)
content = """
<div style="margin: 10px;">
<h4 style="margin: 0 0 10px 0;">{}</h4>
<div style="padding: 10px; border-radius: 3px; max-height: 200px; overflow-y: auto; font-family: monospace; white-space: pre-wrap; font-size: 0.9em;">
{}
</div>
<div style="margin-top: 10px; font-size: 0.9em;">CTRL+click to open file</div>
</div>
""".format(display_name, preview_content)
else:
display_name = "{}#{}".format(file_name, fragment) if fragment else file_name
content = """
<div style="margin: 10px;">
<h4 style="margin: 0 0 10px 0;">{}</h4>
<div style="color: #999; font-style: italic;">File not found</div>
<div style="margin-top: 10px; font-size: 0.9em;">CTRL+click to create</div>
</div>
""".format(display_name)
view.show_popup(
content,
location=point,
max_width=600,
max_height=300,
on_navigate=lambda href: self._navigate_to_file(view, file_name, fragment)
)
def _get_file_preview(self, file_path, fragment=None):
try:
with open(file_path, 'r', encoding='utf-8') as file:
content = file.read()
# If fragment is specified, try to find the section
if fragment:
section_content = self._extract_section(content, fragment)
if section_content:
lines = []
for i, line in enumerate(section_content.split('\n')[:15]):
if i >= 15:
lines.append("...")
break
lines.append(self._escape_html(line))
return "<br>".join(lines) if lines else "<em>Empty section</em>"
# Default: read first few lines (max 15)
lines = []
for i, line in enumerate(content.split('\n')):
if i >= 15:
lines.append("...")
break
lines.append(self._escape_html(line))
return "<br>".join(lines) if lines else "<em>Empty file</em>"
except Exception as e:
return "<em>Error reading file: {}</em>".format(str(e))
def _extract_section(self, content, fragment):
"""Extract section content based on fragment identifier."""
# Try to find markdown-style headers (## Section, # Section, etc.)
# Also supports plain text matching
# Build pattern to match the fragment as a header
# Matches: # Fragment, ## Fragment, ### Fragment, etc.
header_pattern = re.compile(r'^#{1,6}\s+' + re.escape(fragment) + r'\s*$', re.MULTILINE | re.IGNORECASE)
# Also try to match fragment as plain text
text_pattern = re.compile(r'^' + re.escape(fragment) + r'\s*$', re.MULTILINE | re.IGNORECASE)
# Try matching with hyphens/underscores converted to spaces
normalized = fragment.replace('-', ' ').replace('_', ' ')
normalized_header_pattern = re.compile(r'^#{1,6}\s+' + re.escape(normalized) + r'\s*$', re.MULTILINE | re.IGNORECASE)
# Find header match
match = header_pattern.search(content) or text_pattern.search(content) or normalized_header_pattern.search(content)
if not match:
return None
# Found the section header, extract content until next header of same or higher level
start_pos = match.end()
remaining = content[start_pos:]
# Determine header level of the match
matched_text = match.group(0)
header_level = matched_text.count('#') if matched_text.startswith('#') else 1
# Find next header of same or higher level
next_header_pattern = re.compile(r'^#{1,' + str(header_level) + r'}\s+', re.MULTILINE)
next_match = next_header_pattern.search(remaining)
if next_match:
return remaining[:next_match.start()].strip()
else:
return remaining.strip()
def _escape_html(self, text):
return text.replace('&', '&').replace('<', '<').replace('>', '>')
def _find_file_in_workspace(self, view, file_name):
"""Find file in workspace, supporting relative paths and extensions."""
window = view.window()
if not window:
return None
folders = window.folders()
if not folders:
current_file = view.file_name()
if current_file:
folders = [os.path.dirname(current_file)]
else:
return None
# Check if file_name has an extension
has_extension = '.' in os.path.basename(file_name)
# Determine extensions to try
if has_extension:
# File already has an extension, try as-is
extensions = ['']
else:
# No extension, try with common extensions
extensions = ['.txt', '.md', '', '.py', '.html', '.css', '.js']
# Try to resolve relative path from current file's directory first
current_file = view.file_name()
if current_file:
current_dir = os.path.dirname(current_file)
for ext in extensions:
path = os.path.join(current_dir, file_name + ext)
if os.path.isfile(path):
return path
# Try in workspace folders
for folder in folders:
for ext in extensions:
# Try direct join (for relative paths like "main/Adjectives")
path = os.path.join(folder, file_name + ext)
if os.path.isfile(path):
return path
# Try recursive search (for bare filenames without path)
if '/' not in file_name and '\\' not in file_name:
for root, _, files in os.walk(folder):
for file in files:
if file == file_name + ext:
return os.path.join(root, file)
return None
def _navigate_to_file(self, view, file_name, fragment=None):
window = view.window()
if not window:
return
file_path = self._find_file_in_workspace(view, file_name)
if file_path and os.path.isfile(file_path):
# Open the file
opened_view = window.open_file(file_path)
# If there's a fragment, try to navigate to it
if fragment and opened_view:
# Schedule the search to run after the file is loaded
sublime.set_timeout(lambda: self._navigate_to_fragment(opened_view, fragment), 100)
else:
# File doesn't exist, create it
# Check if file_name already has an extension
if '.' in os.path.basename(file_name):
new_file_path = self._get_create_path(view, file_name)
else:
new_file_path = self._get_create_path(view, file_name + '.md')
with open(new_file_path, 'w', encoding='utf-8') as f:
f.write('')
opened_view = window.open_file(new_file_path)
# If there's a fragment, add a header for it
if fragment:
sublime.set_timeout(lambda: self._create_fragment_header(opened_view, fragment), 200)
def _get_create_path(self, view, file_name):
"""Get the path to create a new file."""
window = view.window()
folders = window.folders() if window else []
# If file_name contains a path, resolve it
if '/' in file_name or '\\' in file_name:
# Relative path from current file's directory
current_file = view.file_name()
if current_file:
current_dir = os.path.dirname(current_file)
return os.path.join(current_dir, file_name)
elif folders:
return os.path.join(folders[0], file_name)
# No path info, use first workspace folder or current file's directory
if folders:
return os.path.join(folders[0], file_name)
else:
current_file = view.file_name()
if current_file:
return os.path.join(os.path.dirname(current_file), file_name)
else:
sublime.error_message("Cannot create file '{}': No workspace folder".format(file_name))
return None
def _navigate_to_fragment(self, view, fragment):
"""Navigate to a fragment (section) in the file."""
content = view.substr(sublime.Region(0, view.size()))
section_pos = self._find_fragment_position(content, fragment)
if section_pos is not None:
# Navigate to the section
view.sel().clear()
view.sel().add(sublime.Region(section_pos))
view.show_at_center(section_pos)
def _find_fragment_position(self, content, fragment):
"""Find the position of a fragment in the content."""
# Try to find markdown-style headers (## Section, # Section, etc.)
header_pattern = re.compile(r'^#{1,6}\s+' + re.escape(fragment) + r'\s*$', re.MULTILINE | re.IGNORECASE)
match = header_pattern.search(content)
if match:
return match.start()
# Also try to match fragment as plain text line
text_pattern = re.compile(r'^' + re.escape(fragment) + r'\s*$', re.MULTILINE | re.IGNORECASE)
match = text_pattern.search(content)
if match:
return match.start()
# Try matching with hyphens/underscores converted to spaces
normalized = fragment.replace('-', ' ').replace('_', ' ')
normalized_pattern = re.compile(r'^#{1,6}\s+' + re.escape(normalized) + r'\s*$', re.MULTILINE | re.IGNORECASE)
match = normalized_pattern.search(content)
if match:
return match.start()
return None
def _create_fragment_header(self, view, fragment):
"""Create a header for a new file with a fragment."""
# Insert a markdown header for the fragment
header = "# {}\n\n".format(fragment.replace('-', ' ').replace('_', ' '))
view.run_command('append', {'characters': header})
class WikiLinkNavigateAtCursorCommand(sublime_plugin.TextCommand):
def run(self, edit):
point = self.view.sel()[0].begin()
listener = WikiLinkListener()
link_region = listener._get_link_at_cursor(self.view, point)
if link_region:
file_name, fragment = listener._extract_file_name(self.view, link_region)
listener._navigate_to_file(self.view, file_name, fragment)