Skip to content

Commit 49923e3

Browse files
author
Thomas Scharke
committed
fix: fold TSX object literal attributes
Fixes #14
1 parent 9be598f commit 49923e3

2 files changed

Lines changed: 78 additions & 16 deletions

File tree

src/main/kotlin/dev/zbinski/htmlattributefolder/AttributeFolder.kt

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,53 @@ class AttributeFolder: FoldingBuilderEx(), DumbAware {
2020
for (item in getAttributes(Array(1) { root })) {
2121
var end: Int
2222
var start: Int
23+
2324
if (settings.foldingMethod == 1) {
2425
end = item.attribute.textRange.endOffset
2526
start = item.attribute.textRange.startOffset
2627
} else {
27-
val len = item.attributeName.length + settings.attributeSeparator.length + settings.attributeWrapper.length
28-
start = item.attribute.textRange.startOffset + len
29-
end = item.attribute.textRange.endOffset - settings.attributeWrapper.length
28+
val text = item.attribute.text
29+
val base = item.attribute.textRange.startOffset
30+
31+
// Find '=' then the first non-space char after it
32+
val eq = text.indexOf(settings.attributeSeparator)
33+
if (eq < 0) continue
34+
var i = eq + 1
35+
while (i < text.length && text[i].isWhitespace()) i++
36+
if (i >= text.length) continue
37+
38+
when (text[i]) {
39+
// name="value" or name='value'
40+
'"', '\'' -> {
41+
val quote = text[i]
42+
val open = i
43+
val close = text.indexOf(quote, startIndex = open + 1)
44+
if (close < 0) continue
45+
46+
start = base + open + 1
47+
end = base + close
48+
}
49+
// name={...} or name={{...}}
50+
'{' -> {
51+
val outerOpen = i
52+
val outerClose = findMatchingBrace(text, outerOpen) ?: continue
53+
54+
val isDouble = outerOpen + 1 < text.length && text[outerOpen + 1] == '{'
55+
if (isDouble) {
56+
val innerOpen = outerOpen + 1
57+
val innerClose = findMatchingBrace(text, innerOpen) ?: continue
58+
59+
// Fold inside INNER braces INCLUDING whitespace so output becomes: style={{__PLACEHOLDER__}}
60+
start = base + innerOpen + 1
61+
end = base + innerClose
62+
} else {
63+
// Fold inside single braces INCLUDING whitespace: name={__PLACEHOLDER__}
64+
start = base + outerOpen + 1
65+
end = base + outerClose
66+
}
67+
}
68+
else -> continue
69+
}
3070
}
3171

3272
if (end > start) {
@@ -57,24 +97,46 @@ class AttributeFolder: FoldingBuilderEx(), DumbAware {
5797
return settings.collapseByDefault
5898
}
5999

100+
private fun findMatchingBrace(text: String, openIndex: Int): Int? {
101+
if (openIndex !in text.indices || text[openIndex] != '{') return null
102+
var depth = 0
103+
var i = openIndex
104+
while (i < text.length) {
105+
when (text[i]) {
106+
'{' -> depth++
107+
'}' -> {
108+
depth--
109+
if (depth == 0) return i
110+
}
111+
}
112+
i++
113+
}
114+
return null
115+
}
116+
60117
private fun getAttributes(
61118
elements: Array<PsiElement>,
62119
attributes: ArrayList<String> = settings.attributes
63120
): Sequence<Attribute> = sequence {
64121
for (child in elements) {
122+
val t = child.text
65123
for (attributeName in attributes) {
66-
val attributeBeginning = attributeName + settings.attributeSeparator + settings.attributeWrapper
67-
if (child.text.startsWith(attributeBeginning)) {
124+
val startsLikeAttribute =
125+
t.startsWith("$attributeName=\"") ||
126+
t.startsWith("$attributeName='") ||
127+
t.startsWith("$attributeName={")
128+
129+
if (startsLikeAttribute) {
68130
yield(object : Attribute {
69131
override val attribute = child
70132
override val attributeName = attributeName
71133
})
72134
}
135+
}
73136

74-
val items = getAttributes(child.children, arrayListOf(attributeName)).iterator()
75-
while (items.hasNext()) {
76-
yield(items.next())
77-
}
137+
val items = getAttributes(child.children, attributes).iterator()
138+
while (items.hasNext()) {
139+
yield(items.next())
78140
}
79141
}
80142
}

src/test/kotlin/dev.zbinski.htmlattributefolder/AttributeFolderTSXTest.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,38 @@ package dev.zbinski.htmlattributefolder
22

33
private const val TSX_SNIPPET = """
44
export const Component = () => (
5-
<div class="a b" className="c d" style={{ backgroundColor: "red" }} data-foo="foo-data" />
5+
<div class="a b" className="c d" style={{ backgroundColor: "red", nested: { "foo": "bar" } }} data-foo="foo-data" />
66
)
77
"""
88

99
class AttributeFolderTSXTest : BaseAttributeFolderTest() {
1010
fun testFoldingTSXAttributesCollapsed() {
1111
assertContains(document.text, """class="a b"""")
1212
assertContains(document.text, """className="c d"""")
13-
assertContains(document.text, """style={{ backgroundColor: "red" }}""")
13+
assertContains(document.text, """style={{ backgroundColor: "red", nested: { "foo": "bar" } }}""")
1414
assertContains(document.text, """data-foo="foo-data"""")
1515

16-
configureAttributeFolder(collapseByDefault = true)
16+
configureAttributeFolder(collapseByDefault = true, arrayListOf("class", "className", "style"))
1717

1818
val visualText = applyPluginFoldingAndRender()
1919
assertContains(visualText, """class="__PLACEHOLDER__"""")
2020
assertContains(visualText, """className="__PLACEHOLDER__"""")
21-
assertContains(visualText, """style={{ backgroundColor: "red" }}""")
21+
assertContains(visualText, """style={{__PLACEHOLDER__}}""")
2222
assertContains(visualText, """data-foo="foo-data"""")
2323
}
2424

2525
fun testFoldingTSXAttributesUncollapsed() {
2626
assertContains(document.text, """class="a b"""")
2727
assertContains(document.text, """className="c d"""")
28-
assertContains(document.text, """style={{ backgroundColor: "red" }}""")
28+
assertContains(document.text, """style={{ backgroundColor: "red", nested: { "foo": "bar" } }}""")
2929
assertContains(document.text, """data-foo="foo-data"""")
3030

31-
configureAttributeFolder(collapseByDefault = false)
31+
configureAttributeFolder(collapseByDefault = false, arrayListOf("class", "className", "style"))
3232

3333
val visualText = applyPluginFoldingAndRender()
3434
assertContains(visualText, """class="a b"""")
3535
assertContains(visualText, """className="c d"""")
36-
assertContains(visualText, """style={{ backgroundColor: "red" }}""")
36+
assertContains(visualText, """style={{ backgroundColor: "red", nested: { "foo": "bar" } }}""")
3737
assertContains(visualText, """data-foo="foo-data"""")
3838
}
3939

0 commit comments

Comments
 (0)