Commit decaf5db8c
Verified · cmc
Layout: unified · split
Sources/OrgEditorAppKit/OrgEditor.swift +17 −2
| @@ -59,6 +59,13 @@ public final class OrgEditor: NSObject { | |||
| 59 | /// passes reach it. Always at a line start. | 59 | /// passes reach it. Always at a line start. |
| 60 | private(set) var styledUpTo = 0 | 60 | private(set) var styledUpTo = 0 |
| 61 | private var stylingScheduled = false | 61 | private var stylingScheduled = false |
| 62 | /// One paragraph style object per indent pair, shared by every line that uses it. | ||
| 63 | private var paragraphStyles: [IndentKey: NSParagraphStyle] = [:] | ||
| 64 | |||
| 65 | private struct IndentKey: Hashable { | ||
| 66 | let firstLine: Int | ||
| 67 | let wrapped: Int | ||
| 68 | } | ||
| 62 | 69 | ||
| 63 | /// Styled before the first render: more than a screenful. | 70 | /// Styled before the first render: more than a screenful. |
| 64 | static let firstStyleChunk = 30_000 | 71 | static let firstStyleChunk = 30_000 |
| @@ -253,7 +260,7 @@ public final class OrgEditor: NSObject { | |||
| 253 | for run in Presentation.indents(document.tree, in: range) { | 260 | for run in Presentation.indents(document.tree, in: range) { |
| 254 | guard let clipped = clip(run.range, to: range) else { continue } | 261 | guard let clipped = clip(run.range, to: range) else { continue } |
| 255 | guard run.firstLine == run.wrapped else { | 262 | guard run.firstLine == run.wrapped else { |
| 256 | storage.addAttribute(.paragraphStyle, value: theme.paragraphStyle(firstLine: run.firstLine, wrapped: run.wrapped), range: clipped) | 263 | storage.addAttribute(.paragraphStyle, value: paragraphStyle(run.firstLine, run.wrapped), range: clipped) |
| 257 | continue | 264 | continue |
| 258 | } | 265 | } |
| 259 | // Body lines wrap under their own text: past leading blanks, and past a list bullet | 266 | // Body lines wrap under their own text: past leading blanks, and past a list bullet |
| @@ -263,7 +270,7 @@ public final class OrgEditor: NSObject { | |||
| 263 | let line = text.lineRange(for: NSRange(location: location, length: 0)) | 270 | let line = text.lineRange(for: NSRange(location: location, length: 0)) |
| 264 | let hanging = Self.hangingColumns(text.substring(with: line)) | 271 | let hanging = Self.hangingColumns(text.substring(with: line)) |
| 265 | let lineRange = NSIntersectionRange(line, clipped) | 272 | let lineRange = NSIntersectionRange(line, clipped) |
| 266 | storage.addAttribute(.paragraphStyle, value: theme.paragraphStyle(firstLine: run.firstLine, wrapped: run.wrapped + hanging), range: lineRange) | 273 | storage.addAttribute(.paragraphStyle, value: paragraphStyle(run.firstLine, run.wrapped + hanging), range: lineRange) |
| 267 | location = NSMaxRange(line) | 274 | location = NSMaxRange(line) |
| 268 | } | 275 | } |
| 269 | } | 276 | } |
| @@ -273,6 +280,14 @@ public final class OrgEditor: NSObject { | |||
| 273 | } | 280 | } |
| 274 | } | 281 | } |
| 275 | 282 | ||
| 283 | private func paragraphStyle(_ firstLine: Int, _ wrapped: Int) -> NSParagraphStyle { | ||
| 284 | let key = IndentKey(firstLine: firstLine, wrapped: wrapped) | ||
| 285 | if let style = paragraphStyles[key] { return style } | ||
| 286 | let style = theme.paragraphStyle(firstLine: firstLine, wrapped: wrapped) | ||
| 287 | paragraphStyles[key] = style | ||
| 288 | return style | ||
| 289 | } | ||
| 290 | |||
| 276 | private func clip(_ run: Range<Int>, to range: Range<Int>) -> NSRange? { | 291 | private func clip(_ run: Range<Int>, to range: Range<Int>) -> NSRange? { |
| 277 | let clipped = run.clamped(to: range) | 292 | let clipped = run.clamped(to: range) |
| 278 | return clipped.isEmpty ? nil : NSRange(clipped) | 293 | return clipped.isEmpty ? nil : NSRange(clipped) |