Skip to content

Commit 2527649

Browse files
committed
implement snippet slice de-indentation, and better organize the SnippetParser code
1 parent 3c5cb13 commit 2527649

12 files changed

+612
-343
lines changed

Sources/MarkdownPluginSwift/MarkdownCodeLanguage.Swift.Highlighter.swift

Lines changed: 78 additions & 337 deletions
Large diffs are not rendered by default.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
extension SnippetParser
2+
{
3+
/// A snippet slice is an arbitrary collection of text ranges within a snippet’s source
4+
/// file. Snippet slices are not contiguous, but they should never overlap.
5+
///
6+
/// The individual ranges can span multiple lines, but de-indentation (via ``punch(hole:)``)
7+
/// may further break multiline ranges into multiple single-line ranges.
8+
struct Slice
9+
{
10+
let id:String
11+
var ranges:[Range<Int>]
12+
13+
init(id:String, ranges:[Range<Int>])
14+
{
15+
self.id = id
16+
self.ranges = ranges
17+
}
18+
}
19+
}
20+
extension SnippetParser.Slice
21+
{
22+
mutating
23+
func punch(hole:Range<Int>)
24+
{
25+
guard
26+
let last:Int = self.ranges.indices.last
27+
else
28+
{
29+
preconditionFailure("Punching a hole in an empty slice!")
30+
}
31+
32+
let next:Range<Int>? =
33+
{
34+
if $0.lowerBound < hole.lowerBound
35+
{
36+
let next:Range<Int>? = hole.upperBound < $0.upperBound
37+
? hole.upperBound ..< $0.upperBound
38+
: nil
39+
40+
$0 = $0.lowerBound ..< hole.lowerBound
41+
return next
42+
}
43+
else
44+
{
45+
precondition(hole.upperBound <= $0.upperBound,
46+
"Punching a hole that is not inside the slice!")
47+
48+
$0 = hole.upperBound ..< $0.upperBound
49+
return nil
50+
}
51+
} (&self.ranges[last])
52+
53+
if let next:Range<Int>
54+
{
55+
self.ranges.append(next)
56+
}
57+
}
58+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import SwiftSyntax
2+
3+
extension SnippetParser
4+
{
5+
/// A `SliceBounds` is the precursor to a ``Slice``. It describes the vertical dimensions of
6+
/// a snippet slice, and the indentation of its first marker statement.
7+
struct SliceBounds
8+
{
9+
let id:String
10+
var indent:Int
11+
var ranges:[Range<AbsolutePosition>]
12+
13+
init(id:String, indent:Int)
14+
{
15+
self.id = id
16+
self.indent = indent
17+
self.ranges = []
18+
}
19+
}
20+
}
21+
extension SnippetParser.SliceBounds
22+
{
23+
func viewbox(in utf8:[UInt8]) -> SnippetParser.Slice
24+
{
25+
// We need to do two passes over the source ranges, as indentation is computed across
26+
// the entire slice, and not just one subslice.
27+
let ranges:[Range<Int>] = self.ranges.compactMap
28+
{
29+
let start:Int = $0.lowerBound.utf8Offset
30+
if start >= utf8.endIndex
31+
{
32+
// This could happen if the file ends with a control comment and no
33+
// final newline.
34+
return nil
35+
}
36+
37+
let end:Int = min($0.upperBound.utf8Offset, utf8.endIndex)
38+
if end <= start
39+
{
40+
// Also possible due to missing final newlines.
41+
return nil
42+
}
43+
else
44+
{
45+
return start ..< end
46+
}
47+
}
48+
49+
// Compute maximum removable indentation.
50+
var indent:Int = self.indent
51+
for range:Range<Int> in ranges
52+
{
53+
/// We initialize this to 0 and not nil because we assume the range starts at the
54+
/// beginning of a line.
55+
var spaces:Int? = 0
56+
for byte:UInt8 in utf8[range]
57+
{
58+
switch (byte, spaces)
59+
{
60+
// '\n'
61+
case (0x0A, _):
62+
spaces = 0
63+
64+
// '\r'
65+
case (0x0D, _):
66+
continue
67+
68+
// '\t', ' '
69+
// Tabs and spaces both count as one space. This will work correctly as long as
70+
// people are not mixing tabs and spaces, which no one should be doing anyway.
71+
case (0x09, let count?),
72+
(0x20, let count?):
73+
spaces = count + 1
74+
75+
case (_, let count?):
76+
indent = min(indent, count)
77+
spaces = nil
78+
79+
case (_, nil):
80+
continue
81+
}
82+
}
83+
}
84+
85+
86+
if self.indent == 0
87+
{
88+
return .init(id: self.id, ranges: ranges)
89+
}
90+
91+
var slice:SnippetParser.Slice = .init(id: self.id, ranges: [])
92+
slice.ranges.reserveCapacity(ranges.count)
93+
94+
for range:Range<Int> in ranges
95+
{
96+
slice.ranges.append(range)
97+
98+
print(slice.ranges)
99+
100+
var start:Int? = range.lowerBound
101+
for j:Int in range
102+
{
103+
switch (utf8[j], start)
104+
{
105+
// '\n'
106+
case (0x0A, _):
107+
start = j + 1
108+
109+
// '\r'
110+
// '\t', ' '
111+
case (0x0D, _),
112+
(0x09, _),
113+
(0x20, _):
114+
continue
115+
116+
case (_, let i?):
117+
slice.punch(hole: (i ..< j).prefix(self.indent))
118+
start = nil
119+
120+
case (_, nil):
121+
continue
122+
}
123+
}
124+
}
125+
126+
return slice
127+
}
128+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import SwiftSyntax
2+
3+
extension SnippetParser
4+
{
5+
/// A `SliceFetus` is the precursor to a ``SliceBounds``.
6+
struct SliceFetus
7+
{
8+
private
9+
var slice:SliceBounds
10+
private
11+
var start:AbsolutePosition?
12+
13+
init(id:String, at position:AbsolutePosition, indent:Int = 0)
14+
{
15+
self.slice = .init(id: id, indent: indent)
16+
self.start = position
17+
}
18+
}
19+
}
20+
extension SnippetParser.SliceFetus
21+
{
22+
mutating
23+
func show(at position:AbsolutePosition)
24+
{
25+
if case nil = self.start
26+
{
27+
self.start = position
28+
}
29+
else
30+
{
31+
// TODO: Emit a warning.
32+
}
33+
}
34+
35+
mutating
36+
func hide(at position:AbsolutePosition)
37+
{
38+
guard
39+
let start:AbsolutePosition = self.start
40+
else
41+
{
42+
// TODO: Emit a warning.
43+
return
44+
}
45+
// Two ways this check can fail:
46+
//
47+
// 1. Something resembling a control comment appears in the snippet abstract.
48+
// 2. A snippet slice is hidden instantly after it is shown.
49+
if start < position
50+
{
51+
self.slice.ranges.append(start ..< position)
52+
self.start = nil
53+
}
54+
}
55+
56+
consuming
57+
func end(at position:AbsolutePosition) -> SnippetParser.SliceBounds
58+
{
59+
self.hide(at: position)
60+
return self.slice
61+
}
62+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
extension SnippetParser.SliceMarker
2+
{
3+
enum Statement
4+
{
5+
case end
6+
case hide
7+
case show
8+
case slice(String)
9+
}
10+
}
11+
extension SnippetParser.SliceMarker.Statement
12+
{
13+
private
14+
init(_ text:Substring)
15+
{
16+
switch text
17+
{
18+
case "end": self = .end
19+
case "hide": self = .hide
20+
case "show": self = .show
21+
default: self = .slice(String.init(text))
22+
}
23+
}
24+
25+
init?(lineComment text:borrowing String, skip:Int)
26+
{
27+
guard
28+
let i:String.Index = text.index(text.startIndex,
29+
offsetBy: skip,
30+
limitedBy: text.endIndex)
31+
else
32+
{
33+
fatalError("Encountered a line comment with no leading slashes!")
34+
}
35+
36+
let text:Substring = (copy text)[i...].drop(while: \.isWhitespace)
37+
38+
guard
39+
let j:String.Index = text.firstIndex(of: "."),
40+
case "snippet" = text[..<j]
41+
else
42+
{
43+
return nil
44+
}
45+
46+
let k:String.Index = text.index(after: j)
47+
if let space:String.Index = text[k...].firstIndex(where: \.isWhitespace)
48+
{
49+
guard text[text.index(after: space)...].allSatisfy(\.isWhitespace)
50+
else
51+
{
52+
return nil
53+
}
54+
55+
self.init(text[k ..< space])
56+
}
57+
else
58+
{
59+
self.init(text[k...])
60+
}
61+
}
62+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import SwiftSyntax
2+
3+
extension SnippetParser
4+
{
5+
struct SliceMarker
6+
{
7+
let statement:Statement
8+
/// The number of leading spaces before the control comment.
9+
let indent:Int
10+
/// The UTF-8 offset of the (first) newline before the control comment, or the beginning
11+
/// of the file if the control comment is at the beginning of the file.
12+
let before:AbsolutePosition
13+
/// The UTF-8 offset of the newline after the control comment, assuming it exists.
14+
let after:AbsolutePosition
15+
}
16+
}

0 commit comments

Comments
 (0)