From 9e6d94a27677e7bc1389499dcc796625e2810ca7 Mon Sep 17 00:00:00 2001 From: Khan Winter <35942988+thecoolwinter@users.noreply.github.com> Date: Sat, 15 Jun 2024 20:46:33 -0500 Subject: [PATCH] Fix Page Up/Down Keys (#38) --- .../TextLayoutManager/TextLayoutManager.swift | 8 +- .../SelectionManipulation+Horizontal.swift | 4 +- .../SelectionManipulation+Vertical.swift | 42 ++-- .../TextSelectionManager.swift | 13 +- .../TextView/TextView+FirstResponder.swift | 57 +++++ .../TextView/TextView+KeyDown.swift | 50 +++++ .../TextView/TextView+Layout.swift | 95 +++++++++ .../TextView/TextView+Move.swift | 18 ++ .../TextView/TextView+ScrollToVisible.swift | 61 ++++++ ...extView+TextSelectionManagerDelegate.swift | 18 ++ .../CodeEditTextView/TextView/TextView.swift | 199 ------------------ 11 files changed, 341 insertions(+), 224 deletions(-) create mode 100644 Sources/CodeEditTextView/TextView/TextView+FirstResponder.swift create mode 100644 Sources/CodeEditTextView/TextView/TextView+KeyDown.swift create mode 100644 Sources/CodeEditTextView/TextView/TextView+Layout.swift create mode 100644 Sources/CodeEditTextView/TextView/TextView+ScrollToVisible.swift create mode 100644 Sources/CodeEditTextView/TextView/TextView+TextSelectionManagerDelegate.swift diff --git a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift index d6e9583d..8bd09bd6 100644 --- a/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift +++ b/Sources/CodeEditTextView/TextLayoutManager/TextLayoutManager.swift @@ -189,9 +189,9 @@ public class TextLayoutManager: NSObject { // MARK: - Layout /// Lays out all visible lines - func layoutLines() { // swiftlint:disable:this function_body_length + func layoutLines(in rect: NSRect? = nil) { // swiftlint:disable:this function_body_length guard layoutView?.superview != nil, - let visibleRect = delegate?.visibleRect, + let visibleRect = rect ?? delegate?.visibleRect, !isInTransaction, let textStorage else { return @@ -299,8 +299,8 @@ public class TextLayoutManager: NSObject { var height: CGFloat = 0 var width: CGFloat = 0 - var relativeMinY = max(layoutData.minY - position.yPos, 0) - var relativeMaxY = max(layoutData.maxY - position.yPos, relativeMinY) + let relativeMinY = max(layoutData.minY - position.yPos, 0) + let relativeMaxY = max(layoutData.maxY - position.yPos, relativeMinY) for lineFragmentPosition in line.typesetter.lineFragments.linesStartingAt( relativeMinY, diff --git a/Sources/CodeEditTextView/TextSelectionManager/SelectionManipulation/SelectionManipulation+Horizontal.swift b/Sources/CodeEditTextView/TextSelectionManager/SelectionManipulation/SelectionManipulation+Horizontal.swift index c1165d28..73269247 100644 --- a/Sources/CodeEditTextView/TextSelectionManager/SelectionManipulation/SelectionManipulation+Horizontal.swift +++ b/Sources/CodeEditTextView/TextSelectionManager/SelectionManipulation/SelectionManipulation+Horizontal.swift @@ -36,7 +36,7 @@ package extension TextSelectionManager { ) case .word: return extendSelectionWord(string: string, from: offset, delta: delta) - case .line, .container: + case .line: return extendSelectionLine(string: string, from: offset, delta: delta) case .visualLine: return extendSelectionVisualLine(string: string, from: offset, delta: delta) @@ -46,6 +46,8 @@ package extension TextSelectionManager { } else { return NSRange(location: 0, length: offset) } + case .page: // Not a valid destination horizontally. + return NSRange(location: offset, length: 0) } } diff --git a/Sources/CodeEditTextView/TextSelectionManager/SelectionManipulation/SelectionManipulation+Vertical.swift b/Sources/CodeEditTextView/TextSelectionManager/SelectionManipulation/SelectionManipulation+Vertical.swift index ea8417bf..85294620 100644 --- a/Sources/CodeEditTextView/TextSelectionManager/SelectionManipulation/SelectionManipulation+Vertical.swift +++ b/Sources/CodeEditTextView/TextSelectionManager/SelectionManipulation/SelectionManipulation+Vertical.swift @@ -36,8 +36,8 @@ package extension TextSelectionManager { return extendSelectionVerticalCharacter(from: offset, up: up, suggestedXPos: suggestedXPos) case .word, .line, .visualLine: return extendSelectionVerticalLine(from: offset, up: up) - case .container: - return extendSelectionContainer(from: offset, delta: up ? 1 : -1) + case .page: + return extendSelectionPage(from: offset, delta: up ? 1 : -1, suggestedXPos: suggestedXPos) case .document: if up { return NSRange(location: 0, length: offset) @@ -61,7 +61,7 @@ package extension TextSelectionManager { guard let point = layoutManager?.rectForOffset(offset)?.origin, let newOffset = layoutManager?.textOffsetAtPoint( CGPoint( - x: suggestedXPos == nil ? point.x : suggestedXPos!, + x: suggestedXPos ?? point.x, y: point.y - (layoutManager?.estimateLineHeight() ?? 2.0)/2 * (up ? 1 : -3) ) ) else { @@ -115,22 +115,36 @@ package extension TextSelectionManager { } } - /// Extends a selection one "container" long. + /// Extends a selection one "page" long. /// - Parameters: /// - offset: The location to start extending the selection from. /// - delta: The direction the selection should be extended. `1` for forwards, `-1` for backwards. /// - Returns: The range of the extended selection. - private func extendSelectionContainer(from offset: Int, delta: Int) -> NSRange { - guard let textView, let endOffset = layoutManager?.textOffsetAtPoint( - CGPoint( - x: delta > 0 ? textView.frame.maxX : textView.frame.minX, - y: delta > 0 ? textView.frame.maxY : textView.frame.minY - ) - ) else { + private func extendSelectionPage(from offset: Int, delta: Int, suggestedXPos: CGFloat?) -> NSRange { + guard let textView = textView, + let layoutManager, + let currentYPos = layoutManager.rectForOffset(offset)?.origin.y else { return NSRange(location: offset, length: 0) } - return endOffset > offset - ? NSRange(location: offset, length: endOffset - offset) - : NSRange(location: endOffset, length: offset - endOffset) + + let pageHeight = textView.visibleRect.height + + // Grab the line where the next selection should be. Then use the suggestedXPos to find where in the line the + // selection should be extended to. + layoutManager.layoutLines( + in: NSRect(x: 0, y: currentYPos, width: layoutManager.maxLineWidth, height: pageHeight) + ) + guard let nextPageOffset = layoutManager.textOffsetAtPoint(CGPoint( + x: suggestedXPos ?? 0, + y: min(textView.frame.height, max(0, currentYPos + (delta > 0 ? -pageHeight : pageHeight))) + )) else { + return NSRange(location: offset, length: 0) + } + + if delta > 0 { + return NSRange(location: nextPageOffset, length: offset - nextPageOffset) + } else { + return NSRange(location: offset, length: nextPageOffset - offset) + } } } diff --git a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift index bc7b3955..bcd2ddea 100644 --- a/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift +++ b/Sources/CodeEditTextView/TextSelectionManager/TextSelectionManager.swift @@ -52,8 +52,7 @@ public class TextSelectionManager: NSObject { case word case line case visualLine - /// Eg: Bottom of screen - case container + case page case document } @@ -323,10 +322,12 @@ public class TextSelectionManager: NSObject { let fillRects = getFillRects(in: rect, for: textSelection) - let min = fillRects.min(by: { $0.origin.y < $1.origin.y })?.origin ?? .zero - let max = fillRects.max(by: { $0.origin.y < $1.origin.y }) ?? .zero - let size = CGSize(width: max.maxX - min.x, height: max.maxY - min.y) - textSelection.boundingRect = CGRect(origin: min, size: size) + let minX = fillRects.min(by: { $0.origin.x < $1.origin.x })?.origin.x ?? 0 + let minY = fillRects.min(by: { $0.origin.y < $1.origin.y })?.origin.y ?? 0 + let max = fillRects.max(by: { $0.maxY < $1.maxY }) ?? .zero + let origin = CGPoint(x: minX, y: minY) + let size = CGSize(width: max.maxX - minX, height: max.maxY - minY) + textSelection.boundingRect = CGRect(origin: origin, size: size) context.fill(fillRects) context.restoreGState() diff --git a/Sources/CodeEditTextView/TextView/TextView+FirstResponder.swift b/Sources/CodeEditTextView/TextView/TextView+FirstResponder.swift new file mode 100644 index 00000000..39588a26 --- /dev/null +++ b/Sources/CodeEditTextView/TextView/TextView+FirstResponder.swift @@ -0,0 +1,57 @@ +// +// TextView+FirstResponder.swift +// CodeEditTextView +// +// Created by Khan Winter on 6/15/24. +// + +import AppKit + +extension TextView { + open override func becomeFirstResponder() -> Bool { + isFirstResponder = true + selectionManager.cursorTimer.resetTimer() + needsDisplay = true + return super.becomeFirstResponder() + } + + open override func resignFirstResponder() -> Bool { + isFirstResponder = false + selectionManager.removeCursors() + needsDisplay = true + return super.resignFirstResponder() + } + + open override var canBecomeKeyView: Bool { + super.canBecomeKeyView && acceptsFirstResponder && !isHiddenOrHasHiddenAncestor + } + + /// Sent to the window's first responder when `NSWindow.makeKey()` occurs. + @objc private func becomeKeyWindow() { + _ = becomeFirstResponder() + } + + /// Sent to the window's first responder when `NSWindow.resignKey()` occurs. + @objc private func resignKeyWindow() { + _ = resignFirstResponder() + } + + open override var needsPanelToBecomeKey: Bool { + isSelectable || isEditable + } + + open override var acceptsFirstResponder: Bool { + isSelectable + } + + open override func acceptsFirstMouse(for event: NSEvent?) -> Bool { + return true + } + + open override func resetCursorRects() { + super.resetCursorRects() + if isSelectable { + addCursorRect(visibleRect, cursor: .iBeam) + } + } +} diff --git a/Sources/CodeEditTextView/TextView/TextView+KeyDown.swift b/Sources/CodeEditTextView/TextView/TextView+KeyDown.swift new file mode 100644 index 00000000..1ef36d4f --- /dev/null +++ b/Sources/CodeEditTextView/TextView/TextView+KeyDown.swift @@ -0,0 +1,50 @@ +// +// TextView+KeyDown.swift +// CodeEditTextView +// +// Created by Khan Winter on 6/15/24. +// + +import AppKit +import Carbon.HIToolbox + +extension TextView { + override public func keyDown(with event: NSEvent) { + guard isEditable else { + super.keyDown(with: event) + return + } + + NSCursor.setHiddenUntilMouseMoves(true) + + if !(inputContext?.handleEvent(event) ?? false) { + interpretKeyEvents([event]) + } else { + // Not handled, ignore so we don't double trigger events. + return + } + } + + override public func performKeyEquivalent(with event: NSEvent) -> Bool { + guard isEditable else { + return super.performKeyEquivalent(with: event) + } + + switch Int(event.keyCode) { + case kVK_PageUp: + if !event.modifierFlags.contains(.shift) { + self.pageUp(event) + return true + } + case kVK_PageDown: + if !event.modifierFlags.contains(.shift) { + self.pageDown(event) + return true + } + default: + return false + } + + return false + } +} diff --git a/Sources/CodeEditTextView/TextView/TextView+Layout.swift b/Sources/CodeEditTextView/TextView/TextView+Layout.swift new file mode 100644 index 00000000..11c8ddc5 --- /dev/null +++ b/Sources/CodeEditTextView/TextView/TextView+Layout.swift @@ -0,0 +1,95 @@ +// +// TextView+Layout.swift +// CodeEditTextView +// +// Created by Khan Winter on 6/15/24. +// + +import Foundation + +extension TextView { + open override class var isCompatibleWithResponsiveScrolling: Bool { + true + } + + open override func prepareContent(in rect: NSRect) { + needsLayout = true + super.prepareContent(in: rect) + } + + override public func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + if isSelectable { + selectionManager.drawSelections(in: dirtyRect) + } + } + + override open var isFlipped: Bool { + true + } + + override public var visibleRect: NSRect { + if let scrollView { + var rect = scrollView.documentVisibleRect + rect.origin.y += scrollView.contentInsets.top + return rect.pixelAligned + } else { + return super.visibleRect + } + } + + public var visibleTextRange: NSRange? { + let minY = max(visibleRect.minY, 0) + let maxY = min(visibleRect.maxY, layoutManager.estimatedHeight()) + guard let minYLine = layoutManager.textLineForPosition(minY), + let maxYLine = layoutManager.textLineForPosition(maxY) else { + return nil + } + return NSRange( + location: minYLine.range.location, + length: (maxYLine.range.location - minYLine.range.location) + maxYLine.range.length + ) + } + + public func updatedViewport(_ newRect: CGRect) { + if !updateFrameIfNeeded() { + layoutManager.layoutLines() + } + inputContext?.invalidateCharacterCoordinates() + } + + @discardableResult + public func updateFrameIfNeeded() -> Bool { + var availableSize = scrollView?.contentSize ?? .zero + availableSize.height -= (scrollView?.contentInsets.top ?? 0) + (scrollView?.contentInsets.bottom ?? 0) + let newHeight = max(layoutManager.estimatedHeight(), availableSize.height) + let newWidth = layoutManager.estimatedWidth() + + var didUpdate = false + + if newHeight >= availableSize.height && frame.size.height != newHeight { + frame.size.height = newHeight + // No need to update layout after height adjustment + } + + if wrapLines && frame.size.width != availableSize.width { + frame.size.width = availableSize.width + didUpdate = true + } else if !wrapLines && frame.size.width != max(newWidth, availableSize.width) { + frame.size.width = max(newWidth, availableSize.width) + didUpdate = true + } + + if didUpdate { + needsLayout = true + needsDisplay = true + layoutManager.layoutLines() + } + + if isSelectable { + selectionManager?.updateSelectionViews() + } + + return didUpdate + } +} diff --git a/Sources/CodeEditTextView/TextView/TextView+Move.swift b/Sources/CodeEditTextView/TextView/TextView+Move.swift index 849b4f47..d130f16e 100644 --- a/Sources/CodeEditTextView/TextView/TextView+Move.swift +++ b/Sources/CodeEditTextView/TextView/TextView+Move.swift @@ -158,4 +158,22 @@ extension TextView { selectionManager.moveSelections(direction: .down, destination: .document, modifySelection: true) updateAfterMove() } + + override public func pageUp(_ sender: Any?) { + enclosingScrollView?.pageUp(sender) + } + + override public func pageUpAndModifySelection(_ sender: Any?) { + selectionManager.moveSelections(direction: .up, destination: .page, modifySelection: true) + updateAfterMove() + } + + override public func pageDown(_ sender: Any?) { + enclosingScrollView?.pageDown(sender) + } + + override public func pageDownAndModifySelection(_ sender: Any?) { + selectionManager.moveSelections(direction: .down, destination: .page, modifySelection: true) + updateAfterMove() + } } diff --git a/Sources/CodeEditTextView/TextView/TextView+ScrollToVisible.swift b/Sources/CodeEditTextView/TextView/TextView+ScrollToVisible.swift new file mode 100644 index 00000000..bf1d9ba4 --- /dev/null +++ b/Sources/CodeEditTextView/TextView/TextView+ScrollToVisible.swift @@ -0,0 +1,61 @@ +// +// TextView+ScrollToVisible.swift +// CodeEditTextView +// +// Created by Khan Winter on 6/15/24. +// + +import Foundation + +extension TextView { + fileprivate typealias Direction = TextSelectionManager.Direction + fileprivate typealias TextSelection = TextSelectionManager.TextSelection + + /// Scrolls the upmost selection to the visible rect if `scrollView` is not `nil`. + public func scrollSelectionToVisible() { + guard let scrollView, let selection = getSelection() else { + return + } + + let offsetToScrollTo = offsetNotPivot(selection) + + // There's a bit of a chicken-and-the-egg issue going on here. We need to know the rect to scroll to, but we + // can't know the exact rect to make visible without laying out the text. Then, once text is laid out the + // selection rect may be different again. To solve this, we loop until the frame doesn't change after a layout + // pass and scroll to that rect. + + var lastFrame: CGRect = .zero + while let boundingRect = layoutManager.rectForOffset(offsetToScrollTo), lastFrame != boundingRect { + lastFrame = boundingRect + layoutManager.layoutLines() + selectionManager.updateSelectionViews() + selectionManager.drawSelections(in: visibleRect) + } + if lastFrame != .zero { + scrollView.contentView.scrollToVisible(lastFrame) + } + } + + /// Get the selection that should be scrolled to visible for the current text selection. + /// - Returns: The the selection to scroll to. + private func getSelection() -> TextSelection? { + selectionManager + .textSelections + .sorted(by: { $0.range.max > $1.range.max }) // Get the lowest one. + .first + } + + /// Returns the offset that isn't the pivot of the selection. + /// - Parameter selection: The selection to use. + /// - Returns: The offset suitable for scrolling to. + private func offsetNotPivot(_ selection: TextSelection) -> Int { + guard let pivot = selection.pivot else { + return selection.range.location + } + if selection.range.location == pivot { + return selection.range.max + } else { + return selection.range.location + } + } +} diff --git a/Sources/CodeEditTextView/TextView/TextView+TextSelectionManagerDelegate.swift b/Sources/CodeEditTextView/TextView/TextView+TextSelectionManagerDelegate.swift new file mode 100644 index 00000000..ff105727 --- /dev/null +++ b/Sources/CodeEditTextView/TextView/TextView+TextSelectionManagerDelegate.swift @@ -0,0 +1,18 @@ +// +// TextView+TextSelectionManagerDelegate.swift +// CodeEditTextView +// +// Created by Khan Winter on 6/15/24. +// + +import Foundation + +extension TextView: TextSelectionManagerDelegate { + public func setNeedsDisplay() { + self.setNeedsDisplay(frame) + } + + public func estimatedLineHeight() -> CGFloat { + layoutManager.estimateLineHeight() + } +} diff --git a/Sources/CodeEditTextView/TextView/TextView.swift b/Sources/CodeEditTextView/TextView/TextView.swift index 1f7b97ec..41235559 100644 --- a/Sources/CodeEditTextView/TextView/TextView.swift +++ b/Sources/CodeEditTextView/TextView/TextView.swift @@ -8,10 +8,6 @@ import AppKit import TextStory -// Disabling file length and type body length as the methods and variables contained in this file cannot be moved -// to extensions. -// swiftlint:disable type_body_length - /// # Text View /// /// A view that draws and handles user interactions with text. @@ -336,55 +332,6 @@ public class TextView: NSView, NSTextContent { NSRange(location: 0, length: textStorage.length) } - // MARK: - First Responder - - open override func becomeFirstResponder() -> Bool { - isFirstResponder = true - selectionManager.cursorTimer.resetTimer() - needsDisplay = true - return super.becomeFirstResponder() - } - - open override func resignFirstResponder() -> Bool { - isFirstResponder = false - selectionManager.removeCursors() - needsDisplay = true - return super.resignFirstResponder() - } - - open override var canBecomeKeyView: Bool { - super.canBecomeKeyView && acceptsFirstResponder && !isHiddenOrHasHiddenAncestor - } - - /// Sent to the window's first responder when `NSWindow.makeKey()` occurs. - @objc private func becomeKeyWindow() { - _ = becomeFirstResponder() - } - - /// Sent to the window's first responder when `NSWindow.resignKey()` occurs. - @objc private func resignKeyWindow() { - _ = resignFirstResponder() - } - - open override var needsPanelToBecomeKey: Bool { - isSelectable || isEditable - } - - open override var acceptsFirstResponder: Bool { - isSelectable - } - - open override func acceptsFirstMouse(for event: NSEvent?) -> Bool { - return true - } - - open override func resetCursorRects() { - super.resetCursorRects() - if isSelectable { - addCursorRect(visibleRect, cursor: .iBeam) - } - } - // MARK: - View Lifecycle override public func layout() { @@ -423,137 +370,6 @@ public class TextView: NSView, NSTextContent { } } - // MARK: - Key Down - - override public func keyDown(with event: NSEvent) { - guard isEditable else { - super.keyDown(with: event) - return - } - - NSCursor.setHiddenUntilMouseMoves(true) - - if !(inputContext?.handleEvent(event) ?? false) { - interpretKeyEvents([event]) - } else { - // Handle key events? - } - } - - // MARK: - Layout - - open override class var isCompatibleWithResponsiveScrolling: Bool { - true - } - - open override func prepareContent(in rect: NSRect) { - needsLayout = true - super.prepareContent(in: rect) - } - - override public func draw(_ dirtyRect: NSRect) { - super.draw(dirtyRect) - if isSelectable { - selectionManager.drawSelections(in: dirtyRect) - } - } - - override open var isFlipped: Bool { - true - } - - override public var visibleRect: NSRect { - if let scrollView { - var rect = scrollView.documentVisibleRect - rect.origin.y += scrollView.contentInsets.top - return rect - } else { - return super.visibleRect - } - } - - public var visibleTextRange: NSRange? { - let minY = max(visibleRect.minY, 0) - let maxY = min(visibleRect.maxY, layoutManager.estimatedHeight()) - guard let minYLine = layoutManager.textLineForPosition(minY), - let maxYLine = layoutManager.textLineForPosition(maxY) else { - return nil - } - return NSRange( - location: minYLine.range.location, - length: (maxYLine.range.location - minYLine.range.location) + maxYLine.range.length - ) - } - - public func updatedViewport(_ newRect: CGRect) { - if !updateFrameIfNeeded() { - layoutManager.layoutLines() - } - inputContext?.invalidateCharacterCoordinates() - } - - @discardableResult - public func updateFrameIfNeeded() -> Bool { - var availableSize = scrollView?.contentSize ?? .zero - availableSize.height -= (scrollView?.contentInsets.top ?? 0) + (scrollView?.contentInsets.bottom ?? 0) - let newHeight = max(layoutManager.estimatedHeight(), availableSize.height) - let newWidth = layoutManager.estimatedWidth() - - var didUpdate = false - - if newHeight >= availableSize.height && frame.size.height != newHeight { - frame.size.height = newHeight - // No need to update layout after height adjustment - } - - if wrapLines && frame.size.width != availableSize.width { - frame.size.width = availableSize.width - didUpdate = true - } else if !wrapLines && frame.size.width != max(newWidth, availableSize.width) { - frame.size.width = max(newWidth, availableSize.width) - didUpdate = true - } - - if didUpdate { - needsLayout = true - needsDisplay = true - layoutManager.layoutLines() - } - - if isSelectable { - selectionManager?.updateSelectionViews() - } - - return didUpdate - } - - /// Scrolls the upmost selection to the visible rect if `scrollView` is not `nil`. - public func scrollSelectionToVisible() { - guard let scrollView else { - return - } - - // There's a bit of a chicken-and-the-egg issue going on here. We need to know the rect to scroll to, but we - // can't know the exact rect to make visible without laying out the text. Then, once text is laid out the - // selection rect may be different again. To solve this, we loop until the frame doesn't change after a layout - // pass and scroll to that rect. - - var lastFrame: CGRect = .zero - while let selection = selectionManager - .textSelections - .sorted(by: { $0.boundingRect.origin.y < $1.boundingRect.origin.y }) - .first, - lastFrame != selection.boundingRect { - lastFrame = selection.boundingRect - layoutManager.layoutLines() - selectionManager.updateSelectionViews() - selectionManager.drawSelections(in: visibleRect) - } - if lastFrame != .zero { - scrollView.contentView.scrollToVisible(lastFrame) - } - } - deinit { layoutManager = nil selectionManager = nil @@ -561,18 +377,3 @@ public class TextView: NSView, NSTextContent { NotificationCenter.default.removeObserver(self) } } - -// MARK: - TextSelectionManagerDelegate - -extension TextView: TextSelectionManagerDelegate { - public func setNeedsDisplay() { - self.setNeedsDisplay(frame) - } - - public func estimatedLineHeight() -> CGFloat { - layoutManager.estimateLineHeight() - } -} - -// swiftlint:enable type_body_length -// swiftlint:disable:this file_length