Skip to content

Commit 07ad6b7

Browse files
committed
Toggle Comment command that uses #_ instead of ;;
1 parent a46e66e commit 07ad6b7

File tree

13 files changed

+954
-76
lines changed

13 files changed

+954
-76
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
- Pretty print selection #123
44
- Execute code from inside top-level `; ...` and `#_...` #124
5-
- Clojure-aware `clojure_sublimed_toggle_comment` command
5+
- `Toggle Comment` command that uses `#_` instead of `;;`
66
- Remove background color on quoted strings inside metadata
77
- Better handle eval of `#_` forms in nREPL JVM
88
- Made line numbers much more transparent

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,19 @@ Indent will fix indentation, but will not create new lines. To format a complete
8282
<img src="https://raw.github.com/tonsky/Clojure-Sublimed/master/screenshots/pretty_print.gif" width="830" height="550" alt="Pretty print">
8383

8484

85+
# Toggle comment command
86+
87+
There are three types of comment in Clojure:
88+
89+
- Line comments `;;` are intended for textual / unstructured comments about code
90+
- Reader discard form `#_` is intended for temporarily disabling parts of code
91+
- `comment` form for coding snippets for future use
92+
93+
By default Sublime will try to use `;;` because it’s most similar to other languages and is simple to implmenet. One can argue, however, that we disable/enable parts of the code way more often that write textual comments about them. Therefore `#_` might work better.
94+
95+
Clojure Sublimed offer `Toggle Comment` command that can be used instead of Sublime provided one in Clojure sources. See Keymap on how to enable.
96+
97+
8598
# REPL clients
8699

87100
Clojure Sublimed REPL clients enable interactive development from the comfort of your editor.
@@ -383,6 +396,7 @@ Clear Evaluation Results | <kbd>Ctrl</kbd> <kbd>L</kbd> | <kbd>Ctrl</kb
383396
Copy Evaluation Results | <kbd>Command</kbd> <kbd>C</kbd> | <kbd>Ctrl</kbd> <kbd>C</kbd> | [C]opy
384397
Reindent Lines | <kbd>Ctrl</kbd> <kbd>F</kbd> | <kbd>Ctrl</kbd> <kbd>Alt</kbd> <kbd>F</kbd> | [F]ormat
385398
Reindent Buffer | <kbd>Ctrl</kbd> <kbd>Shift</kbd> <kbd>F</kbd> | <kbd>Ctrl</kbd> <kbd>Alt</kbd> <kbd>Shift</kbd> <kbd>F</kbd> | Capital [F]ormat
399+
Toggle Comment | <kbd>Command</kbd> <kbd>/</kbd> | <kbd>Ctrl</kbd> <kbd>/</kbd> | Comment [/]
386400

387401
To set it up, run `Preferences: Clojure Sublimed Key Bindings` command and copy example keybindings to your local Key Bindings file.
388402

cs_comment.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import re
2+
import sublime, sublime_plugin
3+
from . import cs_parser
4+
5+
def search_point(node, pos):
6+
# no children
7+
if node.nested() is None:
8+
if node.start <= pos and pos <= node.end:
9+
return node
10+
else:
11+
return None
12+
13+
# uncomment
14+
if node.is_terminal() and node.start <= pos and pos <= node.end:
15+
return node
16+
17+
prev_child = None
18+
res = None
19+
for child in node.nested():
20+
# has children: between two (() () | ())
21+
# has children: before first ( | () () ())
22+
if child.start > pos:
23+
res = prev_child
24+
break
25+
26+
# has children: at the start (() |() ())
27+
# has children: inside one (() (|) ())
28+
# has children: at the end (() ()| ())
29+
if child.start <= pos and pos <= child.end:
30+
res = search_point(child, pos)
31+
break
32+
33+
prev_child = child
34+
35+
if res:
36+
return res
37+
38+
if node.start <= pos and pos <= node.end:
39+
return node
40+
41+
def search_range(node, start, end):
42+
if node.nested() is not None and node.start <= start and end <= node.end:
43+
if start <= node.start and node.end <= end:
44+
return [node]
45+
res = []
46+
for child in node.nested():
47+
if child.nested() is not None and child.start <= start and end <= child.end:
48+
return search_range(child, start, end)
49+
# (...)...[...] - no
50+
# (...[...)...]
51+
# [...(...)...]
52+
# [...(...]...)
53+
# [...]...(...) - no
54+
# (...[...]...)
55+
elif not child.end <= start and not child.start >= end:
56+
res.append(child)
57+
return res or [node]
58+
59+
class ClojureSublimedToggleCommentCommand(sublime_plugin.TextCommand):
60+
def run(self, edit):
61+
view = self.view
62+
parsed = cs_parser.parse_tree(view)
63+
sel = []
64+
offset = 0
65+
regions = [r for r in view.sel()]
66+
regions.sort(key = lambda r: r.begin())
67+
for region in regions:
68+
if region.empty():
69+
nodes = [search_point(parsed, region.begin())]
70+
else:
71+
nodes = search_range(parsed, region.begin(), region.end())
72+
a = region.a + offset
73+
b = region.b + offset
74+
if all(node.name == "comment" for node in nodes):
75+
# uncomment line comments
76+
for node in nodes:
77+
m = re.match(r'^;+\s*', node.text)
78+
r = sublime.Region(node.start + offset, node.start + offset + len(m[0]))
79+
view.replace(edit, r, "")
80+
a = a if a < r.begin() else r.begin() if a < r.end() else a - r.size()
81+
b = b if b < r.begin() else r.begin() if b < r.end() else b - r.size()
82+
offset -= r.size()
83+
elif all(node.name in ["discard", "comment"] for node in nodes):
84+
# uncomment discards only
85+
for node in nodes:
86+
if node.name == "discard":
87+
r = sublime.Region(node.start + offset, node.body.start + offset)
88+
view.replace(edit, r, "")
89+
a = a if a < r.begin() else r.begin() if a < r.end() else a - r.size()
90+
b = b if b < r.begin() else r.begin() if b < r.end() else b - r.size()
91+
offset -= r.size()
92+
else:
93+
for node in nodes:
94+
if node.name not in ["discard", "comment"]:
95+
# comment
96+
r = sublime.Region(node.start + offset, node.start + offset)
97+
view.replace(edit, r, "#_")
98+
a = a if a < r.begin() else a + 2
99+
b = b if b < r.begin() else b + 2
100+
offset += 2
101+
sel.append(sublime.Region(a, b))
102+
view.sel().clear()
103+
view.sel().add_all(sel)

cs_indent.py

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -159,34 +159,6 @@ def run(self, edit):
159159
formatted = cs_printer.format(form, node, limit = cs_common.wrap_width(view))
160160
view.replace(edit, region, formatted)
161161

162-
class ClojureSublimedToggleCommentCommand(sublime_plugin.TextCommand):
163-
def run(self, edit):
164-
view = self.view
165-
change_id = view.change_id()
166-
for region in [r for r in view.sel()]:
167-
region = view.transform_region_from(region, change_id)
168-
for line in [l for l in view.lines(region)]:
169-
line = view.transform_region_from(line, change_id)
170-
line_text = view.substr(line)
171-
172-
# skip leading spaces
173-
m = re.match(r'^\s*', line_text)
174-
line = sublime.Region(line.begin() + len(m[0]), line.end())
175-
line_text = view.substr(line)
176-
177-
# uncomment ;
178-
if m := re.match(r'^(\s*)(;[;\s]*)(.*)', line_text):
179-
view.replace(edit, line, m[1] + m[3])
180-
continue
181-
parsed = cs_parser.parse_tree(view)
182-
# uncomment #_
183-
if node := cs_parser.search(parsed, line.begin(), pred = lambda node: node.name == "discard"):
184-
view.replace(edit, sublime.Region(node.start, node.body.start), '')
185-
continue
186-
# prepend ;
187-
if m := re.match(r'^(\s*)([^\s].*)', line_text):
188-
view.replace(edit, line, m[1] + '; ' + m[2])
189-
190162
def cljfmt_indent(view, point):
191163
i = None
192164
try:

cs_parser.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ def __getattr__(self, name):
3232
def restore_text(self, text):
3333
return text[self.start:self.end]
3434

35+
def is_terminal(self):
36+
return self.name in ["discard", "wrap", "meta", "tagged"]
37+
38+
def nested(self):
39+
if self.name in ["brackets", "braces", "parens"]:
40+
return self.body.children if self.body else []
41+
elif self.name == "source":
42+
return self.children
43+
3544
class Named:
3645
"""
3746
Parser that assigns name to Node

script/sublime.py

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def __iter__(self):
4545
return iter((self.a, self.b))
4646

4747
def __str__(self) -> str:
48-
return "(" + str(self.a) + ", " + str(self.b) + ")"
48+
return "Region(" + str(self.a) + ", " + str(self.b) + ")"
4949

5050
def __repr__(self) -> str:
5151
if self.xpos == -1:
@@ -160,6 +160,131 @@ def intersects(self, region: Region) -> bool:
160160
(rb > lb and rb < le) or (re > lb and re < le) or
161161
(lb > rb and lb < re) or (le > rb and le < re))
162162

163+
class Selection:
164+
"""
165+
Maintains a set of sorted non-overlapping Regions. A selection may be
166+
empty.
167+
168+
This is primarily used to represent the textual selection.
169+
"""
170+
171+
def __init__(self, regions):
172+
self.regions = list(regions)
173+
174+
def __iter__(self) -> Iterator[Region]:
175+
"""
176+
Iterate through all the regions in the selection.
177+
178+
.. since:: 4023 3.8
179+
"""
180+
return iter(self.regions)
181+
182+
def __len__(self) -> int:
183+
""" :returns: The number of regions in the selection. """
184+
return len(self.regions)
185+
186+
def __getitem__(self, index: int) -> Region:
187+
""" :returns: The region at the given ``index``. """
188+
return self.regions(index)
189+
190+
def __delitem__(self, index: int):
191+
""" Delete the region at the given ``index``. """
192+
del self.regions[index]
193+
194+
def __eq__(self, rhs: object) -> bool:
195+
""" :returns: Whether the selections are identical. """
196+
return rhs is not None and isinstance(rhs, Selection) and list(self) == list(rhs)
197+
198+
def __lt__(self, rhs: Optional[Selection]) -> bool:
199+
""" """
200+
return rhs is not None and list(self) < list(rhs)
201+
202+
def __bool__(self) -> bool:
203+
""" The selection is ``True`` when not empty. """
204+
return len(self) > 0
205+
206+
def __str__(self) -> str:
207+
return f"{self!r}[{', '.join(map(str, self))}]"
208+
209+
def __repr__(self) -> str:
210+
return f'Selection'
211+
212+
# def is_valid(self) -> bool:
213+
# """ :returns: Whether this selection is for a valid view. """
214+
# return True
215+
216+
def clear(self):
217+
""" Remove all regions from the selection. """
218+
self.regions.clear()
219+
220+
def add(self, x: Region):
221+
"""
222+
Add a `Region` or `Point` to the selection. It will be merged with the
223+
existing regions if intersecting.
224+
"""
225+
self.regions.append(x)
226+
227+
def add_all(self, regions: Iterable[Region]):
228+
""" Add all the regions from the given iterable. """
229+
for r in regions:
230+
self.add(r)
231+
232+
# def subtract(self, region: Region):
233+
# """
234+
# Subtract a region from the selection, such that the whole region is no
235+
# longer contained within the selection.
236+
# """
237+
# sublime_api.view_selection_subtract_region(self.view_id, region.a, region.b)
238+
239+
# def contains(self, region: Region) -> bool:
240+
# """ :returns: Whether the provided region is contained within the selection. """
241+
# return sublime_api.view_selection_contains(self.view_id, region.a, region.b)
242+
243+
class View:
244+
def __init__(self, text="", sel = None):
245+
self.text = text
246+
self.text_lines = text.split('\n')
247+
self._sel = Selection(sel) if sel else Selection([Region(0, 0)])
248+
249+
def sel(self):
250+
return self._sel
251+
252+
def rowcol(self, point):
253+
row = 0
254+
offset = 0
255+
for line in self.text_lines:
256+
line_len = len(line) + 1
257+
if offset + line_len <= point:
258+
row += 1
259+
offset += line_len
260+
else:
261+
col = point - offset
262+
return (row, col)
263+
return (row, offset)
264+
# lines = self.text[:point].split('\n')
265+
# row = len(lines) - 1
266+
# col = len(lines[-1]) if lines else 0
267+
# return (row, col)
268+
269+
def substr(self, region):
270+
return self.text[region.begin():region.end()]
271+
272+
def size(self):
273+
return len(self.text)
274+
275+
def replace(self, edit, region, new_text):
276+
self.text = self.text[:region.begin()] + new_text + self.text[region.end():]
277+
self.text_lines = self.text.split('\n')
278+
279+
def lines(self, region):
280+
start = 0
281+
res = []
282+
for line in self.text_lines:
283+
if start + len(line) >= region.begin() or start < region.end():
284+
res.append(Region(start, start + len(line)))
285+
start += len(line) + 1
286+
return res
287+
163288
platform = {'Darwin': 'osx', 'Linux': 'linux', 'Windows': 'windows'}[platform.system()]
164289

165290
def platform() -> Literal["osx", "linux", "windows"]:

script/sublime_plugin.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ class EventListener:
22
pass
33

44
class TextCommand:
5-
pass
5+
def __init__(self, view):
6+
self.view: sublime.View = view

script/test_cljfmt.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
sublime = importlib.import_module(module + '.script.sublime')
1010
test_core = importlib.import_module(module + '.script.test_core')
1111

12-
def test_printer():
12+
def test():
1313
def test_fn(input):
1414
return cs_cljfmt.format_string(input, cwd = dir)
1515
test_core.run_tests(dir + "/test_indent/", test_fn, col_input = False)
1616

1717
if __name__ == '__main__':
18-
test_printer()
18+
test()

0 commit comments

Comments
 (0)