import Foundation import SuggestionModel import Workspace public struct FilespaceSuggestionSnapshot: Equatable { public var linesHash: Int public var cursorPosition: CursorPosition public init(linesHash: Int, cursorPosition: CursorPosition) { self.linesHash = linesHash self.cursorPosition = cursorPosition } } public struct FilespaceSuggestionSnapshotKey: FilespacePropertyKey { public static func createDefaultValue() -> FilespaceSuggestionSnapshot { .init(linesHash: -1, cursorPosition: .outOfScope) } } public extension FilespacePropertyValues { @WorkspaceActor var suggestionSourceSnapshot: FilespaceSuggestionSnapshot { get { self[FilespaceSuggestionSnapshotKey.self] } set { self[FilespaceSuggestionSnapshotKey.self] = newValue } } } public extension Filespace { @WorkspaceActor func resetSnapshot() { // swiftformat:disable redundantSelf self.suggestionSourceSnapshot = FilespaceSuggestionSnapshotKey.createDefaultValue() // swiftformat:enable all } /// Validate the suggestion is still valid. /// - Parameters: /// - lines: lines of the file /// - cursorPosition: cursor position /// - Returns: `true` if the suggestion is still valid @WorkspaceActor func validateSuggestions(lines: [String], cursorPosition: CursorPosition) -> Bool { guard let presentingSuggestion else { return false } // cursor has moved to another line if cursorPosition.line != presentingSuggestion.position.line { reset() resetSnapshot() return false } // the cursor position is valid guard cursorPosition.line >= 0, cursorPosition.line < lines.count else { reset() resetSnapshot() return false } let editingLine = lines[cursorPosition.line].dropLast(1) // dropping \n let suggestionLines = presentingSuggestion.text.split(separator: "\n") let suggestionFirstLine = suggestionLines.first ?? "" /// For example: /// ``` /// ABCD012 // typed text /// ^ /// 0123456 // suggestion range 4-11, generated after `ABCD` /// ``` /// The suggestion should contain `012`, aka, the suggestion that is typed. /// /// Another case is that the suggestion may contain the whole line. /// /// ``` /// ABCD012 // typed text /// ----^ /// ABCD0123456 // suggestion range 0-11, generated after `ABCD` /// The suggestion should contain `ABCD012`, aka, the suggestion that is typed. /// ``` let typedSuggestion = { let startIndex = editingLine.index( editingLine.startIndex, offsetBy: presentingSuggestion.range.start.character, limitedBy: editingLine.endIndex ) ?? editingLine.startIndex let endIndex = editingLine.index( editingLine.startIndex, offsetBy: cursorPosition.character, limitedBy: editingLine.endIndex ) ?? editingLine.endIndex if endIndex > startIndex { return editingLine[startIndex.. 0, !suggestionFirstLine.hasPrefix(typedSuggestion) { reset() resetSnapshot() return false } // finished typing the whole suggestion when the suggestion has only one line if typedSuggestion.hasPrefix(suggestionFirstLine), suggestionLines.count <= 1 { reset() resetSnapshot() return false } // undo to a state before the suggestion was generated if editingLine.count < presentingSuggestion.position.character { reset() resetSnapshot() return false } return true } }