<commit_before>//
//  SyntaxParser.swift
//
//  CotEditor
//  https://coteditor.com
//
//  Created by 1024jp on 2018-04-28.
//
//  ---------------------------------------------------------------------------
//
//  © 2004-2007 nakamuxu
//  © 2014-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 Foundation
import AppKit.NSTextStorage

protocol SyntaxParserDelegate: AnyObject {
    
    func syntaxParser(_ syntaxParser: SyntaxParser, didParseOutline outlineItems: [OutlineItem])
    func syntaxParser(_ syntaxParser: SyntaxParser, didStartParsingOutline progress: Progress)
}


private extension NSAttributedString.Key {
    
    static let syntaxType = NSAttributedString.Key("CotEditor.SyntaxType")
}



// MARK: -

final class SyntaxParser {
    
    static let didUpdateOutlineNotification = Notification.Name("SyntaxStyleDidUpdateOutline")
    
    private struct Cache {
        
        var styleName: String
        var string: String
        var highlights: [SyntaxType: [NSRange]]
    }
    
    
    // MARK: Public Properties
    
    let textStorage: NSTextStorage
    
    var style: SyntaxStyle
    
    weak var delegate: SyntaxParserDelegate?
    
    private(set) var outlineItems: [OutlineItem] = [] {
        
        didSet {
            // inform about outline items update
            DispatchQueue.main.async { [weak self, items = outlineItems] in
                guard let self = self else { return assertionFailure() }
                
                self.delegate?.syntaxParser(self, didParseOutline: items)
                NotificationCenter.default.post(name: SyntaxParser.didUpdateOutlineNotification, object: self)
            }
        }
    }
    
    
    // MARK: Private Properties
    
    private let outlineParseOperationQueue = OperationQueue(name: "com.coteditor.CotEditor.outlineParseOperationQueue")
    private let syntaxHighlightParseOperationQueue = OperationQueue(name: "com.coteditor.CotEditor.syntaxHighlightParseOperationQueue")
    
    private var highlightCache: Cache?  // results cache of the last whole string highlights
    
    private lazy var outlineUpdateTask = Debouncer(delay: .milliseconds(400)) { [weak self] in self?.parseOutline() }
    
    
    
    // MARK: -
    // MARK: Lifecycle
    
    init(textStorage: NSTextStorage, style: SyntaxStyle = SyntaxStyle()) {
        
        self.textStorage = textStorage
        self.style = style
    }
    
    
    deinit {
        self.invalidateCurrentParse()
    }
    
    
    
    // MARK: Public Methods
    
    /// Whether syntax should be parsed.
    var canParse: Bool {
        
        return UserDefaults.standard[.enableSyntaxHighlight] && !self.style.isNone
    }
    
    
    /// Cancel all syntax parse including ones in the queues.
    func invalidateCurrentParse() {
        
        self.highlightCache = nil
        self.outlineUpdateTask.cancel()
        self.outlineParseOperationQueue.cancelAllOperations()
        self.syntaxHighlightParseOperationQueue.cancelAllOperations()
    }
    
}



// MARK: - Outline

extension SyntaxParser {
    
    /// Parse outline with delay.
    func invalidateOutline() {
        
        guard
            self.canParse,
            !self.style.outlineExtractors.isEmpty
            else {
                self.outlineItems = []
                return
        }
        
        self.outlineUpdateTask.schedule()
    }
    
    
    
    // MARK: Private Methods
    
    /// Perform outline parse.
    private func parseOutline() {
        
        let wholeRange = self.textStorage.range
        guard !wholeRange.isEmpty else {
            self.outlineItems = []
            return
        }
        
        let operation = OutlineParseOperation(extractors: self.style.outlineExtractors,
                                              string: self.textStorage.string.immutable,
                                              range: wholeRange)
        
        operation.completionBlock = { [weak self, weak operation] in
            guard let operation = operation, !operation.isCancelled else { return }
            
            self?.outlineItems = operation.results
        }
        operation.qualityOfService = .utility
        
        // -> Regarding the outline extraction, just cancel previous operations before parsing the latest string,
        //    since user cannot cancel it manually.
        self.outlineParseOperationQueue.cancelAllOperations()
        
        self.outlineParseOperationQueue.addOperation(operation)
        
        self.delegate?.syntaxParser(self, didStartParsingOutline: operation.progress)
    }
    
}



// MARK: - Syntax Highlight

extension SyntaxParser {
    
    /// Update whole syntax highlights.
    ///
    /// - Parameter completionHandler: The block to execute when the process completes.
    /// - Returns: The progress of the async highlight task if performed.
    func highlightAll(forcesParsing: Bool = false, completionHandler: @escaping (() -> Void) = {}) -> Progress? {
        
        assert(Thread.isMainThread)
        
        guard UserDefaults.standard[.enableSyntaxHighlight] else { return nil }
        guard !self.textStorage.string.isEmpty else { return nil }
        
        guard !self.style.isNone else {
            self.textStorage.apply(highlights: [:], range: self.textStorage.range)
            return nil
        }
        
        let wholeRange = self.textStorage.range
        
        // use cache if the content of the whole document is the same as the last
        if
            let cache = self.highlightCache,
            cache.styleName == self.style.name,
            cache.string == self.textStorage.string
        {
            self.textStorage.apply(highlights: cache.highlights, range: wholeRange)
            completionHandler()
            return nil
        }
        
        // make sure that string is immutable
        // -> `string` of NSTextStorage is actually a mutable object
        //    and it can cause crash when a mutable string is given to NSRegularExpression instance.
        //    (2016-11, macOS 10.12.1 SDK)
        let string = self.textStorage.string.immutable
        
        return self.highlight(string: string, range: wholeRange, completionHandler: completionHandler)<commit_msg>Remove unused argument<commit_after>//
//  SyntaxParser.swift
//
//  CotEditor
//  https://coteditor.com
//
//  Created by 1024jp on 2018-04-28.
//
//  ---------------------------------------------------------------------------
//
//  © 2004-2007 nakamuxu
//  © 2014-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 Foundation
import AppKit.NSTextStorage

protocol SyntaxParserDelegate: AnyObject {
    
    func syntaxParser(_ syntaxParser: SyntaxParser, didParseOutline outlineItems: [OutlineItem])
    func syntaxParser(_ syntaxParser: SyntaxParser, didStartParsingOutline progress: Progress)
}


private extension NSAttributedString.Key {
    
    static let syntaxType = NSAttributedString.Key("CotEditor.SyntaxType")
}



// MARK: -

final class SyntaxParser {
    
    static let didUpdateOutlineNotification = Notification.Name("SyntaxStyleDidUpdateOutline")
    
    private struct Cache {
        
        var styleName: String
        var string: String
        var highlights: [SyntaxType: [NSRange]]
    }
    
    
    // MARK: Public Properties
    
    let textStorage: NSTextStorage
    
    var style: SyntaxStyle
    
    weak var delegate: SyntaxParserDelegate?
    
    private(set) var outlineItems: [OutlineItem] = [] {
        
        didSet {
            // inform about outline items update
            DispatchQueue.main.async { [weak self, items = outlineItems] in
                guard let self = self else { return assertionFailure() }
                
                self.delegate?.syntaxParser(self, didParseOutline: items)
                NotificationCenter.default.post(name: SyntaxParser.didUpdateOutlineNotification, object: self)
            }
        }
    }
    
    
    // MARK: Private Properties
    
    private let outlineParseOperationQueue = OperationQueue(name: "com.coteditor.CotEditor.outlineParseOperationQueue")
    private let syntaxHighlightParseOperationQueue = OperationQueue(name: "com.coteditor.CotEditor.syntaxHighlightParseOperationQueue")
    
    private var highlightCache: Cache?  // results cache of the last whole string highlights
    
    private lazy var outlineUpdateTask = Debouncer(delay: .milliseconds(400)) { [weak self] in self?.parseOutline() }
    
    
    
    // MARK: -
    // MARK: Lifecycle
    
    init(textStorage: NSTextStorage, style: SyntaxStyle = SyntaxStyle()) {
        
        self.textStorage = textStorage
        self.style = style
    }
    
    
    deinit {
        self.invalidateCurrentParse()
    }
    
    
    
    // MARK: Public Methods
    
    /// Whether syntax should be parsed.
    var canParse: Bool {
        
        return UserDefaults.standard[.enableSyntaxHighlight] && !self.style.isNone
    }
    
    
    /// Cancel all syntax parse including ones in the queues.
    func invalidateCurrentParse() {
        
        self.highlightCache = nil
        self.outlineUpdateTask.cancel()
        self.outlineParseOperationQueue.cancelAllOperations()
        self.syntaxHighlightParseOperationQueue.cancelAllOperations()
    }
    
}



// MARK: - Outline

extension SyntaxParser {
    
    /// Parse outline with delay.
    func invalidateOutline() {
        
        guard
            self.canParse,
            !self.style.outlineExtractors.isEmpty
            else {
                self.outlineItems = []
                return
        }
        
        self.outlineUpdateTask.schedule()
    }
    
    
    
    // MARK: Private Methods
    
    /// Perform outline parse.
    private func parseOutline() {
        
        let wholeRange = self.textStorage.range
        guard !wholeRange.isEmpty else {
            self.outlineItems = []
            return
        }
        
        let operation = OutlineParseOperation(extractors: self.style.outlineExtractors,
                                              string: self.textStorage.string.immutable,
                                              range: wholeRange)
        
        operation.completionBlock = { [weak self, weak operation] in
            guard let operation = operation, !operation.isCancelled else { return }
            
            self?.outlineItems = operation.results
        }
        operation.qualityOfService = .utility
        
        // -> Regarding the outline extraction, just cancel previous operations before parsing the latest string,
        //    since user cannot cancel it manually.
        self.outlineParseOperationQueue.cancelAllOperations()
        
        self.outlineParseOperationQueue.addOperation(operation)
        
        self.delegate?.syntaxParser(self, didStartParsingOutline: operation.progress)
    }
    
}



// MARK: - Syntax Highlight

extension SyntaxParser {
    
    /// Update whole syntax highlights.
    ///
    /// - Parameter completionHandler: The block to execute when the process completes.
    /// - Returns: The progress of the async highlight task if performed.
    func highlightAll(completionHandler: @escaping (() -> Void) = {}) -> Progress? {
        
        assert(Thread.isMainThread)
        
        guard UserDefaults.standard[.enableSyntaxHighlight] else { return nil }
        guard !self.textStorage.string.isEmpty else { return nil }
        
        guard !self.style.isNone else {
            self.textStorage.apply(highlights: [:], range: self.textStorage.range)
            return nil
        }
        
        let wholeRange = self.textStorage.range
        
        // use cache if the content of the whole document is the same as the last
        if
            let cache = self.highlightCache,
            cache.styleName == self.style.name,
            cache.string == self.textStorage.string
        {
            self.textStorage.apply(highlights: cache.highlights, range: wholeRange)
            completionHandler()
            return nil
        }
        
        // make sure that string is immutable
        // -> `string` of NSTextStorage is actually a mutable object
        //    and it can cause crash when a mutable string is given to NSRegularExpression instance.
        //    (2016-11, macOS 10.12.1 SDK)
        let string = self.textStorage.string.immutable
        
        return self.highlight(string: string, range: wholeRange, completionHandler: completionHandler)
{'fraction_non_alphanumeric': 0.09019521503008954, 'fraction_numerical': 0.007338910905621606, 'mean_word_length': 1.994286969896726, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 11, 'https://': 4, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3851157', 'n_tokens_mistral': 3606, 'n_tokens_neox': 3431, 'n_words': 956}