<commit_before>//
//  NSTextView+CurrentLineHighlighting.swift
//
//  CotEditor
//  https://coteditor.com
//
//  Created by 1024jp on 2018-08-18.
//
//  ---------------------------------------------------------------------------
//
//  © 2018-2020 1024jp
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//  https://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

import AppKit

protocol CurrentLineHighlighting: NSTextView {
    
    var needsUpdateLineHighlight: Bool { get set }
    var lineHighLightRects: [NSRect] { get set }
    var lineHighLightColor: NSColor? { get }
}



extension CurrentLineHighlighting {
    
    // MARK: Public Methods
    
    /// draw current line highlight
    func drawCurrentLine(in dirtyRect: NSRect) {
        
        if self.needsUpdateLineHighlight {
            self.lineHighLightRects = self.calcurateLineHighLightRects()
            self.needsUpdateLineHighlight = false
        }
        
        let rects = self.lineHighLightRects
            .filter { $0.intersects(dirtyRect) }
            .map { self.centerScanRect($0) }
            .compactMap { $0.intersection(dirtyRect) }
        
        guard
            !rects.isEmpty,
            let color = self.lineHighLightColor
            else { return }
        
        NSGraphicsContext.saveGraphicsState()
        color.setFill()
        for rect in rects {
            rect.fill()
        }
        NSGraphicsContext.restoreGraphicsState()
    }
    
    
    
    // MARK: Private Methods
    
    /// Calculate highlight rects for all insertion points.
    ///
    /// - Returns: Rects for current line highlight.
    private func calcurateLineHighLightRects() -> [NSRect] {
        
        return self.rangesForUserTextChange?
            .map(\.rangeValue)
            .map { (self.string as NSString).lineContentsRange(for: $0) }
            .unique
            .map { self.lineRect(for: $0) }
            ?? []
    }
    
    
    /// Return rect for the line that contains the given range.
    ///
    /// - Parameter range: The range to obtain line rect.
    /// - Returns: Line rect in view coordinate.
    private func lineRect(for range: NSRange) -> NSRect {
        
        guard
            let textContainer = self.textContainer,
            let layoutManager = self.layoutManager
            else { assertionFailure(); return .zero }
        
        return layoutManager.lineFragmentsRect(for: range)
            .insetBy(dx: textContainer.lineFragmentPadding, dy: 0)
            .offset(by: self.textContainerOrigin)
    }
    
}



private extension NSLayoutManager {
    
    func lineFragmentsRect(for range: NSRange) -> NSRect {
        
        guard<commit_msg>Optimize current line rect calculation<commit_after>//
//  NSTextView+CurrentLineHighlighting.swift
//
//  CotEditor
//  https://coteditor.com
//
//  Created by 1024jp on 2018-08-18.
//
//  ---------------------------------------------------------------------------
//
//  © 2018-2020 1024jp
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//  https://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
//

import AppKit

protocol CurrentLineHighlighting: NSTextView {
    
    var needsUpdateLineHighlight: Bool { get set }
    var lineHighLightRects: [NSRect] { get set }
    var lineHighLightColor: NSColor? { get }
}



extension CurrentLineHighlighting {
    
    // MARK: Public Methods
    
    /// draw current line highlight
    func drawCurrentLine(in dirtyRect: NSRect) {
        
        if self.needsUpdateLineHighlight {
            self.lineHighLightRects = self.calcurateLineHighLightRects()
            self.needsUpdateLineHighlight = false
        }
        
        let rects = self.lineHighLightRects
            .filter { $0.intersects(dirtyRect) }
            .map { self.centerScanRect($0) }
            .compactMap { $0.intersection(dirtyRect) }
        
        guard
            !rects.isEmpty,
            let color = self.lineHighLightColor
            else { return }
        
        NSGraphicsContext.saveGraphicsState()
        color.setFill()
        for rect in rects {
            rect.fill()
        }
        NSGraphicsContext.restoreGraphicsState()
    }
    
    
    
    // MARK: Private Methods
    
    /// Calculate highlight rects for all insertion points.
    ///
    /// - Returns: Rects for current line highlight.
    private func calcurateLineHighLightRects() -> [NSRect] {
        
        guard let editingRanges = self.rangesForUserTextChange else { return [] }
        
        return editingRanges
            .map(\.rangeValue)
            .map { (self.string as NSString).lineRange(for: $0) }
            .reduce(into: [NSRange]()) { (ranges, range) in
                if ranges.last?.touches(range) == true {
                    ranges[ranges.count - 1].formUnion(range)
                } else {
                    ranges.append(range)
                }
            }
            .map { self.lineRect(for: $0) }
    }
    
    
    /// Return rect for the line that contains the given range.
    ///
    /// - Parameter range: The range to obtain line rect.
    /// - Returns: Line rect in view coordinate.
    private func lineRect(for range: NSRange) -> NSRect {
        
        guard
            let textContainer = self.textContainer,
            let layoutManager = self.layoutManager
            else { assertionFailure(); return .zero }
        
        return layoutManager.lineFragmentsRect(for: range)
            .insetBy(dx: textContainer.lineFragmentPadding, dy: 0)
            .offset(by: self.textContainerOrigin)
    }
    
}



private extension NSLayoutManager {
    
    func lineFragmentsRect(for range: NSRange) -> NSRect {
        
        guard
{'fraction_non_alphanumeric': 0.11298838437170011, 'fraction_numerical': 0.01040880977523005, 'mean_word_length': 1.8176795580110496, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 9, 'https://': 4, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1561303', 'n_tokens_mistral': 1856, 'n_tokens_neox': 1758, 'n_words': 506}