Skip to content

Commit 0c0d11e

Browse files
committed
Tests for indentation, cljfmt default config
1 parent b5f0246 commit 0c0d11e

File tree

10 files changed

+765
-12
lines changed

10 files changed

+765
-12
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
### WIP
22

3+
- Simplified formatting rules: if list's first form is a symbol, indent next line by +2 spaces, in all other cases, indent to opening paren (1 space)
4+
- We now provide `cljfmt.edn` that tries to match our default formatting
5+
- Better handle selection after formatting with cljfmt
36
- Highlight namespace name as `entity.name`, same as defs
47
- No exceptions on disconnect
58
- Removed background on unused symbols inside quotes
6-
- Simplified formatting rules: if list's first form is a symbol, indent next line by +2 spaces, in all other cases, indent to opening paren (1 space)
7-
- Better handle selection after formatting with cljfmt
89

910
### 4.1.1 - Sep 6, 2024
1011

cljfmt.edn

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{:indents {#re ".*" [[:inner 0]]}
2+
:remove-surrounding-whitespace? false
3+
:remove-trailing-whitespace? false
4+
:remove-consecutive-blank-lines? false}

cs_cljfmt.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import difflib, os, re, sublime, subprocess
22
from . import cs_common, cs_parser
33

4-
def format_string(view, text):
4+
def format_string(text, view = None, cwd = None):
55
try:
66
cmd = 'cljfmt.exe' if 'windows' == sublime.platform() else 'cljfmt'
7-
cwd = None
8-
if file := view.file_name():
9-
cwd = os.path.dirname(file)
10-
elif folders := view.window().folders():
11-
cwd = folders[0]
7+
if not cwd:
8+
if file := view.file_name():
9+
cwd = os.path.dirname(file)
10+
elif folders := view.window().folders():
11+
cwd = folders[0]
1212

1313
proc = subprocess.run([cmd, 'fix', '-'],
1414
input = text,
@@ -29,7 +29,7 @@ def indent_lines(view, regions, edit):
2929
replacements = []
3030
for region in regions:
3131
text = view.substr(region)
32-
if text_formatted := format_string(view, text):
32+
if text_formatted := format_string(text, view = view):
3333
pos = region.begin()
3434
diff = difflib.ndiff(text.splitlines(keepends=True), text_formatted.splitlines(keepends=True))
3535
for line in diff:
@@ -75,7 +75,7 @@ def newline_indent(view, point):
7575
if to_close and '"' == to_close[0]:
7676
return None
7777
text = text[start:] + "\nCLOJURE_SUBLIMED_SYM" + "".join(to_close)
78-
formatted = format_string(view, text)
78+
formatted = format_string(text, view = view)
7979
last_line = formatted.splitlines()[-1]
8080
indent = re.match(r"^\s*", last_line)[0]
8181
return len(indent)

cs_indent.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,10 +104,11 @@ def indent_lines(view, selections, edit):
104104
replacements[row] = (begin, delta_i)
105105

106106
# Now apply all replacements, recalculating begins as we go
107-
change_id = view.change_id()
107+
delta_total = 0
108108
for row in replacements:
109109
begin, delta_i = replacements[row]
110-
begin = view.transform_region_from(sublime.Region(begin, begin), change_id).begin()
110+
begin = begin + delta_total
111+
delta_total += delta_i
111112
if delta_i < 0:
112113
view.replace(edit, sublime.Region(begin, begin - delta_i), "")
113114
else:

script/sublime.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Don't evaluate type annotations at runtime
2+
from __future__ import annotations
3+
from typing import Literal, Optional
4+
import platform
5+
6+
Point = int
7+
DIP = float
8+
9+
class Region:
10+
"""
11+
A singular selection region. This region has a order - ``b`` may be before
12+
or at ``a``.
13+
14+
Also commonly used to represent an area of the text buffer, where ordering
15+
and ``xpos`` are generally ignored.
16+
"""
17+
18+
__slots__ = ['a', 'b', 'xpos']
19+
20+
def __init__(self, a: Point, b: Optional[Point] = None, xpos: DIP = -1):
21+
""" """
22+
if b is None:
23+
b = a
24+
25+
self.a: Point = a
26+
""" The first end of the region. """
27+
self.b: Point = b
28+
"""
29+
The second end of the region. In a selection this is the location of the
30+
caret. May be less than ``a``.
31+
"""
32+
self.xpos: DIP = xpos
33+
"""
34+
In a selection this is the target horizontal position of the region.
35+
This affects behavior when pressing the up or down keys. Use ``-1`` if
36+
undefined.
37+
"""
38+
39+
def __iter__(self):
40+
"""
41+
Iterate through all the points in the region.
42+
43+
.. since:: 4023 3.8
44+
"""
45+
return iter((self.a, self.b))
46+
47+
def __str__(self) -> str:
48+
return "(" + str(self.a) + ", " + str(self.b) + ")"
49+
50+
def __repr__(self) -> str:
51+
if self.xpos == -1:
52+
return f'Region({self.a}, {self.b})'
53+
return f'Region({self.a}, {self.b}, xpos={self.xpos})'
54+
55+
def __len__(self) -> int:
56+
""" :returns: The size of the region. """
57+
return self.size()
58+
59+
def __eq__(self, rhs: object) -> bool:
60+
"""
61+
:returns: Whether the two regions are identical. Ignores ``xpos``.
62+
"""
63+
return isinstance(rhs, Region) and self.a == rhs.a and self.b == rhs.b
64+
65+
def __lt__(self, rhs: Region) -> bool:
66+
"""
67+
:returns: Whether this region starts before the rhs. The end of the
68+
region is used to resolve ties.
69+
"""
70+
lhs_begin = self.begin()
71+
rhs_begin = rhs.begin()
72+
73+
if lhs_begin == rhs_begin:
74+
return self.end() < rhs.end()
75+
else:
76+
return lhs_begin < rhs_begin
77+
78+
def __contains__(self, v: Region | Point) -> bool:
79+
"""
80+
:returns: Whether the provided `Region` or `Point` is entirely contained
81+
within this region.
82+
83+
.. since:: 4023 3.8
84+
"""
85+
if isinstance(v, Region):
86+
return v.a in self and v.b in self
87+
elif isinstance(v, int):
88+
return v >= self.begin() and v <= self.end()
89+
else:
90+
fq_name = ""
91+
if v.__class__.__module__ not in {'builtins', '__builtin__'}:
92+
fq_name = f"{v.__class__.__module__}."
93+
fq_name += v.__class__.__qualname__
94+
raise TypeError(
95+
"in <Region> requires int or Region as left operand"
96+
f", not {fq_name}")
97+
98+
def to_tuple(self) -> tuple[Point, Point]:
99+
"""
100+
.. since:: 4075
101+
102+
:returns: This region as a tuple ``(a, b)``.
103+
"""
104+
return (self.a, self.b)
105+
106+
def empty(self) -> bool:
107+
""" :returns: Whether the region is empty, ie. ``a == b``. """
108+
return self.a == self.b
109+
110+
def begin(self) -> Point:
111+
""" :returns: The smaller of ``a`` and ``b``. """
112+
if self.a < self.b:
113+
return self.a
114+
else:
115+
return self.b
116+
117+
def end(self) -> Point:
118+
""" :returns: The larger of ``a`` and ``b``. """
119+
if self.a < self.b:
120+
return self.b
121+
else:
122+
return self.a
123+
124+
def size(self) -> int:
125+
""" Equivalent to `__len__`. """
126+
return abs(self.a - self.b)
127+
128+
def contains(self, x: Point) -> bool:
129+
""" Equivalent to `__contains__`. """
130+
return x in self
131+
132+
def cover(self, region: Region) -> Region:
133+
""" :returns: A `Region` spanning both regions. """
134+
a = min(self.begin(), region.begin())
135+
b = max(self.end(), region.end())
136+
137+
if self.a < self.b:
138+
return Region(a, b)
139+
else:
140+
return Region(b, a)
141+
142+
def intersection(self, region: Region) -> Region:
143+
""" :returns: A `Region` covered by both regions. """
144+
if self.end() <= region.begin():
145+
return Region(0)
146+
if self.begin() >= region.end():
147+
return Region(0)
148+
149+
return Region(max(self.begin(), region.begin()), min(self.end(), region.end()))
150+
151+
def intersects(self, region: Region) -> bool:
152+
""" :returns: Whether the provided region intersects this region. """
153+
lb = self.begin()
154+
le = self.end()
155+
rb = region.begin()
156+
re = region.end()
157+
158+
return (
159+
(lb == rb and le == re) or
160+
(rb > lb and rb < le) or (re > lb and re < le) or
161+
(lb > rb and lb < re) or (le > rb and le < re))
162+
163+
platform = {'Darwin': 'osx', 'Linux': 'linux', 'Windows': 'windows'}[platform.system()]
164+
165+
def platform() -> Literal["osx", "linux", "windows"]:
166+
"""
167+
:returns: The platform which the plugin is being run on.
168+
"""
169+
return platform
170+
171+
def error_message(msg: str):
172+
""" Display an error dialog. """
173+
print('ERROR:', msg, file = sys.stderr, flush = True)

script/sublime_plugin.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
class EventListener:
2+
pass
3+
4+
class TextCommand:
5+
pass

script/test_cljfmt.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#! /usr/bin/env python3
2+
import importlib, os, sys
3+
4+
dir = os.path.abspath(os.path.dirname(__file__) + "/..")
5+
module = os.path.basename(dir)
6+
sys.path.append(os.path.abspath(dir + "/.."))
7+
8+
cs_cljfmt = importlib.import_module(module + '.cs_cljfmt')
9+
sublime = importlib.import_module(module + '.script.sublime')
10+
test_core = importlib.import_module(module + '.script.test_core')
11+
12+
def test_printer():
13+
def test_fn(input):
14+
return cs_cljfmt.format_string(input, cwd = dir)
15+
test_core.run_tests(dir + "/test_indent/", test_fn, col_input = False)
16+
17+
if __name__ == '__main__':
18+
test_printer()

script/test_indent.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#! /usr/bin/env python3
2+
import importlib, os, sys
3+
4+
dir = os.path.abspath(os.path.dirname(__file__) + "/..")
5+
module = os.path.basename(dir)
6+
sys.path.append(os.path.abspath(dir + "/.."))
7+
8+
cs_indent = importlib.import_module(module + '.cs_indent')
9+
sublime = importlib.import_module(module + '.script.sublime')
10+
test_core = importlib.import_module(module + '.script.test_core')
11+
12+
class View:
13+
def __init__(self, text=""):
14+
self.text = text
15+
16+
def rowcol(self, point):
17+
lines = self.text[:point].split('\n')
18+
row = len(lines) - 1
19+
col = len(lines[-1]) if lines else 0
20+
return (row, col)
21+
22+
def substr(self, region):
23+
return self.text[region.begin():region.end()]
24+
25+
def size(self):
26+
return len(self.text)
27+
28+
def replace(self, edit, region, new_text):
29+
self.text = self.text[:region.begin()] + new_text + self.text[region.end():]
30+
31+
def lines(self, region):
32+
lines = self.text.split('\n')
33+
start = 0
34+
res = []
35+
for line in lines:
36+
if start + len(line) >= region.begin() or start < region.end():
37+
res.append(sublime.Region(start, start + len(line)))
38+
start += len(line) + 1
39+
return res
40+
41+
def test_printer():
42+
def test_fn(input):
43+
view = View(input)
44+
cs_indent.indent_lines(view, [sublime.Region(0, view.size())], None)
45+
return view.text
46+
test_core.run_tests(dir + "/test_indent/", test_fn, col_input = False)
47+
48+
if __name__ == '__main__':
49+
test_printer()

0 commit comments

Comments
 (0)