// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/omnibox/ui/omnibox_text_view_ios.h"

#import <CoreText/CoreText.h>

#import "base/apple/foundation_util.h"
#import "base/check_is_test.h"
#import "base/check_op.h"
#import "base/command_line.h"
#import "base/ios/ios_util.h"
#import "base/metrics/user_metrics.h"
#import "base/not_fatal_until.h"
#import "base/notreached.h"
#import "base/strings/sys_string_conversions.h"
#import "components/grit/components_scaled_resources.h"
#import "components/omnibox/browser/autocomplete_input.h"
#import "components/open_from_clipboard/clipboard_async_wrapper_ios.h"
#import "ios/chrome/browser/autocomplete/model/autocomplete_scheme_classifier_impl.h"
#import "ios/chrome/browser/composebox/public/features.h"
#import "ios/chrome/browser/omnibox/public/omnibox_constants.h"
#import "ios/chrome/browser/omnibox/public/omnibox_ui_features.h"
#import "ios/chrome/browser/omnibox/public/omnibox_util.h"
#import "ios/chrome/browser/omnibox/ui/omnibox_text_input.h"
#import "ios/chrome/browser/omnibox/ui/omnibox_text_input_delegate.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/public/features/system_flags.h"
#import "ios/chrome/browser/shared/ui/util/animation_util.h"
#import "ios/chrome/browser/shared/ui/util/reversed_animation.h"
#import "ios/chrome/browser/shared/ui/util/rtl_geometry.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/toolbar/legacy/ui_bundled/public/toolbar_constants.h"
#import "ios/chrome/common/material_timing.h"
#import "ios/chrome/common/ui/colors/semantic_color_names.h"
#import "ios/chrome/common/ui/util/dynamic_type_util.h"
#import "ios/chrome/common/ui/util/ui_util.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/grit/ios_theme_resources.h"
#import "skia/ext/skia_utils_ios.h"
#import "ui/base/device_form_factor.h"
#import "ui/base/l10n/l10n_util_mac.h"
#import "ui/gfx/color_palette.h"
#import "ui/gfx/image/image.h"
#import "ui/gfx/scoped_cg_context_save_gstate_mac.h"

using enum OmniboxKeyboardAction;

namespace {

/// The placeholder leading padding.
const CGFloat kPlaceholderLeadingPadding = 5.0;

/// The vertical offset added to the text view. This is to align with the
/// OmniboxTextFieldIOS that OmniboxTextViewIOS replaces.
const CGFloat kVerticalOffset = 1;

}  // namespace

@interface OmniboxTextViewIOS () <UIGestureRecognizerDelegate,
                                  UITextViewDelegate>

@property(nonatomic, assign, getter=isPreEditing) BOOL preEditing;

/// Optional text displayed after user and autocomplete text.
@property(nonatomic, strong) NSAttributedString* attributedAdditionalText;

@end

@implementation OmniboxTextViewIOS {
  /// Length of autocomplete text.
  NSUInteger _autocompleteTextLength;
  /// Tap gesture recognizer for this view.
  UITapGestureRecognizer* _tapGestureRecognizer;
  /// Whether the pasteboard currently has strings.
  BOOL _pasteboardHasStrings;
  OmniboxPresentationContext _presentationContext;
  /// The default foreground color for text.
  UIColor* _defaultTextColor;

  /// The omnibox typing attributes.
  NSDictionary<NSAttributedStringKey, id>* _omniboxTypingAttributes;

  /// Whether to force disable the return key.
  BOOL _forceDisableReturnKey;

  // The default and custom placeholder texts to be shown when there is no other
  // text present.
  NSString* _customPlaceholderText;
  NSString* _defaultPlaceholderText;

  // Constraint for the placeholder label top anchor.
  NSLayoutConstraint* _placeholderTopConstraint;

  // Cached single line height.
  CGFloat _cachedSingleLineHeight;

  // Whether a newline is being programmatically inserted (e.g. by
  // Shift+Return).
  BOOL _insertingNewline;
}

@synthesize omniboxTextInputDelegate = _omniboxTextInputDelegate;
@synthesize omniboxKeyboardDelegate = _omniboxKeyboardDelegate;
@synthesize clearingPreEditText = _clearingPreEditText;
@synthesize allowsReturnKeyWithEmptyText = _allowsReturnKeyWithEmptyText;
@synthesize editing = _editing;

#pragma mark - Public methods

- (instancetype)initWithFrame:(CGRect)frame
                    textColor:(UIColor*)textColor
                    tintColor:(UIColor*)tintColor
          presentationContext:(OmniboxPresentationContext)presentationContext {
  self = [super initWithFrame:frame];
  if (self) {
    self.backgroundColor = [UIColor clearColor];
    if (tintColor) {
      [self setTintColor:tintColor];
    }
    _presentationContext = presentationContext;
    _defaultTextColor = textColor;
    self.textColor = textColor;
    self.autocorrectionType = UITextAutocorrectionTypeNo;
    self.autocapitalizationType = UITextAutocapitalizationTypeNone;
    self.enablesReturnKeyAutomatically = YES;
    BOOL isCobrowse =
        _presentationContext == OmniboxPresentationContext::kCobrowse;
    self.returnKeyType = isCobrowse ? UIReturnKeyDefault : UIReturnKeyGo;
    self.spellCheckingType = UITextSpellCheckingTypeNo;
    self.textAlignment = NSTextAlignmentNatural;
    self.keyboardType =
        isCobrowse ? UIKeyboardTypeDefault : UIKeyboardTypeWebSearch;
    self.smartQuotesType = UITextSmartQuotesTypeNo;
    self.dataDetectorTypes = UIDataDetectorTypeNone;
    self.allowsEditingTextAttributes = NO;
    self.writingToolsBehavior = UIWritingToolsBehaviorNone;
    [self updateOmniboxTypingAttributes];

    // Disable drag on iPhone because there's nowhere to drag to
    if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_TABLET) {
      self.textDragInteraction.enabled = NO;
    }

    // Remove drop when presented by the Composebox so that the composebox can
    // handle all drop actions.
    if (_presentationContext == OmniboxPresentationContext::kComposebox) {
      [self removeInteraction:self.textDropInteraction];
    }

    // Force initial layout of internal text label.
    self.font = self.currentFont;
    [self updateTextContainerInset];

    _tapGestureRecognizer =
        [[UITapGestureRecognizer alloc] initWithTarget:self
                                                action:@selector(handleTap:)];
    _tapGestureRecognizer.delegate = self;
    [self addGestureRecognizer:_tapGestureRecognizer];

    [[NSNotificationCenter defaultCenter]
        addObserver:self
           selector:@selector(pasteboardDidChange:)
               name:UIPasteboardChangedNotification
             object:nil];

    [self pasteboardDidChange:nil];

    [[NSNotificationCenter defaultCenter]
        addObserver:self
           selector:@selector(applicationDidBecomeActive:)
               name:UIApplicationDidBecomeActiveNotification
             object:nil];

    self.delegate = self;

    [self registerForTraitChanges:@[ UITraitPreferredContentSizeCategory.class ]
                       withAction:@selector(updateTextProperitesOnTraitChange)];
  }
  return self;
}

- (instancetype)initWithCoder:(NSCoder*)aDecoder {
  NOTREACHED();
}

- (void)setPlaceholderLabel:(UILabel*)placeholderLabel {
  placeholderLabel.font = self.font;
  placeholderLabel.textColor = [UIColor colorNamed:kTextSecondaryColor];
  placeholderLabel.isAccessibilityElement = NO;
  _placeholderLabel = placeholderLabel;

  // Align placeholder with the text view's content area by constraining it
  // directly to the text view's frame and then adding the internal insets.
  UIEdgeInsets textInsets = self.textContainerInset;
  _placeholderTopConstraint =
      [placeholderLabel.topAnchor constraintEqualToAnchor:self.topAnchor
                                                 constant:textInsets.top];
  [NSLayoutConstraint activateConstraints:@[
    _placeholderTopConstraint,
    [placeholderLabel.leadingAnchor
        constraintEqualToAnchor:self.leadingAnchor
                       constant:kPlaceholderLeadingPadding],
    [placeholderLabel.trailingAnchor
        constraintEqualToAnchor:self.trailingAnchor],
  ]];
}

- (void)setAllowsReturnKeyWithEmptyText:(BOOL)allowsReturnKeyWithEmptyText {
  if (_allowsReturnKeyWithEmptyText == allowsReturnKeyWithEmptyText) {
    return;
  }
  _allowsReturnKeyWithEmptyText = allowsReturnKeyWithEmptyText;
  [self reloadInputViews];
}

- (void)setMinimumHeight:(CGFloat)minimumHeight {
  if (_minimumHeight == minimumHeight) {
    return;
  }
  _minimumHeight = minimumHeight;
  [self updateTextContainerInset];
}

- (void)setText:(NSAttributedString*)text
    userTextLength:(size_t)userTextLength {
  DCHECK_LE(userTextLength, text.length);
  if (userTextLength > 0) {
    [self exitPreEditState];
  }

  NSUInteger autocompleteLength = text.length - userTextLength;
  [self setTextInternal:text autocompleteLength:autocompleteLength];
}

- (void)setAdditionalText:(NSString*)additionalText {
  if (!additionalText.length) {
    self.attributedAdditionalText = nil;
    return;
  }
  NSMutableAttributedString* additionalAttributedText =
      [[NSMutableAttributedString alloc] initWithString:additionalText];
  [additionalAttributedText
      addAttributes:@{
        NSForegroundColorAttributeName :
            [UIColor colorNamed:kTextSecondaryColor]
      }
              range:NSMakeRange(0, additionalAttributedText.length)];
  self.attributedAdditionalText = additionalAttributedText;
}

- (void)insertTextWhileEditing:(NSString*)text {
  // This method should only be called while editing.
  DCHECK([self isFirstResponder]);

  if ([self markedTextRange] != nil) {
    [self unmarkText];
  }

  NSRange selectedNSRange = [self selectedNSRange];
  if (!self.delegate || [self.delegate textView:self
                            shouldChangeTextInRange:selectedNSRange
                                    replacementText:text]) {
    [self replaceRange:[self selectedTextRange] withText:text];
  }
}

- (NSString*)autocompleteText {
  if ([self hasAutocompleteText]) {
    // In crbug.com/1237851, sometimes _autocompleteTextLength is greater
    // than self.text.length, causing the subtraction below to overflow,
    // breaking
    // `-substringToIndex:`. This shouldn't happen, so use the DCHECK to catch
    // it to help debug and default to the end of the string if an overflow
    // would occur.
    DCHECK_LE(_autocompleteTextLength, self.text.length);
    const NSUInteger totalLength = self.text.length;
    const NSUInteger userTextEndIndex = totalLength - [self addedTextLength];
    if (userTextEndIndex + _autocompleteTextLength > totalLength) {
      return @"";
    }
    return [self.text substringWithRange:NSMakeRange(userTextEndIndex,
                                                     _autocompleteTextLength)];
  }
  return @"";
}

- (NSString*)userText {
  // In crbug.com/1237851, sometimes `_autocompleteTextLength` is greater than
  // self.text.length, causing the subtraction below to overflow, breaking
  // `-substringToIndex:`. This shouldn't happen, so use the DCHECK to catch it
  // to help debug and default to the end of the string if an overflow would
  // occur.
  const NSUInteger addedTextLength = [self addedTextLength];
  DCHECK_LE(addedTextLength, self.text.length);
  NSUInteger userTextEndIndex = self.text.length >= addedTextLength
                                    ? self.text.length - addedTextLength
                                    : self.text.length;
  return [self.text substringToIndex:userTextEndIndex];
}

- (NSAttributedString*)attributedUserText {
  const NSUInteger addedTextLength = [self addedTextLength];
  DCHECK_LE(addedTextLength, self.text.length);
  NSUInteger userTextEndIndex = self.text.length >= addedTextLength
                                    ? self.text.length - addedTextLength
                                    : self.text.length;
  return [self.attributedText
      attributedSubstringFromRange:NSMakeRange(0, userTextEndIndex)];
}

- (NSString*)markedText {
  DCHECK([self conformsToProtocol:@protocol(UITextInput)]);
  return [self textInRange:[self markedTextRange]];
}

- (NSString*)displayedText {
  return [self textWithoutAdditionalText].string;
}

- (BOOL)hasAutocompleteText {
  return _autocompleteTextLength > 0;
}

- (void)clearAutocompleteText {
  if ([self hasAutocompleteText]) {
    self.text = self.userText;
  }
  if ([self hasAdditionalText]) {
    [self removeAdditionalText];
  }
}

- (void)setAttributedAdditionalText:
    (NSAttributedString*)attributedAdditionalText {
  [self removeAdditionalText];

  if (!attributedAdditionalText.length) {
    return;
  }

  NSAttributedString* currentText = self.attributedText;
  _attributedAdditionalText = attributedAdditionalText;
  [self setTextInternal:currentText autocompleteLength:_autocompleteTextLength];
}

- (NSRange)selectedNSRange {
  DCHECK([self isFirstResponder]);
  UITextPosition* beginning = [self beginningOfDocument];
  UITextRange* selectedRange = [self selectedTextRange];
  NSInteger start = [self offsetFromPosition:beginning
                                  toPosition:[selectedRange start]];
  NSInteger length = [self offsetFromPosition:[selectedRange start]
                                   toPosition:[selectedRange end]];
  return NSMakeRange(start, length);
}

- (NSTextAlignment)bestTextAlignment {
  if ([self isFirstResponder]) {
    return DetermineBestAlignmentForText(self.text);
  }
  return NSTextAlignmentNatural;
}

- (UISemanticContentAttribute)bestSemanticContentAttribute {
  // This method will be called in response to
  // UITextInputCurrentInputModeDidChangeNotification. At this
  // point, the baseWritingDirectionForPosition doesn't yet return the correct
  // direction if the text field is empty. Instead, treat this as a special case
  // and calculate the direction from the keyboard locale if there is no text.
  if (self.text.length == 0 ||
      _presentationContext == OmniboxPresentationContext::kLensOverlay) {
    NSLocaleLanguageDirection direction = [NSLocale
        characterDirectionForLanguage:self.textInputMode.primaryLanguage];
    return direction == NSLocaleLanguageDirectionRightToLeft
               ? UISemanticContentAttributeForceRightToLeft
               : UISemanticContentAttributeForceLeftToRight;
  }

  [self setTextAlignment:NSTextAlignmentNatural];

  NSWritingDirection textDirection =
      [self baseWritingDirectionForPosition:[self beginningOfDocument]
                                inDirection:UITextStorageDirectionForward];
  NSLocaleLanguageDirection currentLocaleDirection = [NSLocale
      characterDirectionForLanguage:NSLocale.currentLocale.languageCode];

  if ((textDirection == NSWritingDirectionLeftToRight &&
       currentLocaleDirection == NSLocaleLanguageDirectionLeftToRight) ||
      (textDirection == NSWritingDirectionRightToLeft &&
       currentLocaleDirection == NSLocaleLanguageDirectionRightToLeft)) {
    return UISemanticContentAttributeUnspecified;
  }

  if (textDirection == NSWritingDirectionNatural) {
    return self.semanticContentAttribute;
  }

  return textDirection == NSWritingDirectionRightToLeft
             ? UISemanticContentAttributeForceRightToLeft
             : UISemanticContentAttributeForceLeftToRight;
}

// Normally NSTextAlignmentNatural would handle text alignment automatically,
// but there are numerous edge case issues with it, so it's simpler to just
// manually update the text alignment and writing direction of the UITextField.
- (void)updateTextDirection {
  // If the keyboard language direction does not match the device
  // language direction, the alignment of the placeholder text will be off.
  NSLocaleLanguageDirection direction = [NSLocale
      characterDirectionForLanguage:self.textInputMode.primaryLanguage];
  if (direction == NSLocaleLanguageDirectionRightToLeft) {
    [self setTextAlignment:NSTextAlignmentRight];
  } else {
    [self setTextAlignment:NSTextAlignmentLeft];
  }
  self.placeholderLabel.textAlignment = self.textAlignment;
}

#pragma mark - UI Refresh animation public helpers

- (CGFloat)offsetForString:(NSString*)string {
  // Sometimes `string` is not contained in self.text, for example for
  // https://en.m.wikipedia.org/foo the `string` might be "en.wikipedia.org" if
  // the scheme and the "m." trivial subdomain are stripped. In this case,
  // default to a reasonable prefix string to give a plausible offset.
  NSString* prefixString = @"https://";

  if (string.length > 0 && [self.text containsString:string]) {
    NSRange range = [self.text rangeOfString:string];
    NSRange prefixRange = NSMakeRange(0, range.location);
    prefixString = [self.text substringWithRange:prefixRange];
  }

  return [prefixString
             sizeWithAttributes:@{NSFontAttributeName : self.currentFont}]
      .width;
}

#pragma mark pre-edit

/// Enters pre-edit state.
- (void)enterPreEditState {
  // Empty omnibox should show the insertion point immediately. There is
  // nothing to erase.
  if (!self.text.length || UIAccessibilityIsVoiceOverRunning() ||
      self.preEditing) {
    return;
  }

  NSMutableDictionary<NSAttributedStringKey, id>* attributes =
      self.typingAttributes.mutableCopy;
  [attributes setValue:self.currentFont forKey:NSFontAttributeName];
  [attributes setValue:self.selectedTextBackgroundColor
                forKey:NSBackgroundColorAttributeName];

  // Also apply the attributes to the whole text.
  NSMutableAttributedString* attributedText = [self.attributedText mutableCopy];
  [attributedText addAttributes:attributes
                          range:NSMakeRange(0, self.attributedText.length)];
  self.attributedText = attributedText;

  self.preEditing = YES;
  [self.heightDelegate textViewContentChanged:self];
}

/// Exits pre-edit state.
- (void)exitPreEditState {
  if (!self.preEditing) {
    return;
  }
  self.preEditing = NO;

  NSMutableDictionary<NSAttributedStringKey, id>* attributes =
      self.typingAttributes.mutableCopy;
  [attributes setValue:self.currentFont forKey:NSFontAttributeName];
  [attributes setValue:UIColor.clearColor
                forKey:NSBackgroundColorAttributeName];
  self.typingAttributes = attributes;

  if (!self.clearingPreEditText) {
    // Also apply the attributes to the whole text.
    NSMutableAttributedString* attributedText =
        [self.attributedText mutableCopy];
    [attributedText addAttributes:attributes
                            range:NSMakeRange(0, self.attributedText.length)];
    self.attributedText = attributedText;
    [self.heightDelegate textViewContentChanged:self];
  }
}

#pragma mark - UITextView

// Ensures that attributedText always uses the proper style attributes.
- (void)setAttributedText:(NSAttributedString*)attributedText {
  NSMutableAttributedString* mutableText = [attributedText mutableCopy];
  NSRange entireString = NSMakeRange(0, [mutableText length]);

  // Set the font.
  [mutableText addAttribute:NSFontAttributeName
                      value:self.currentFont
                      range:entireString];

  // When editing, use the default text color for all text, except the
  // additionnal text.
  if (self.editing) {
    NSRange foregroundColorRange = entireString;
    if ([self hasAdditionalText]) {
      foregroundColorRange = NSMakeRange(
          0, mutableText.length - self.attributedAdditionalText.length);
    }
    [mutableText addAttribute:NSForegroundColorAttributeName
                        value:_defaultTextColor
                        range:foregroundColorRange];
  } else {
    NSMutableParagraphStyle* style = [[NSMutableParagraphStyle alloc] init];
    // URLs have their text direction set to to LTR (avoids RTL characters
    // making the URL render from right to left, as per the URL rendering
    // standard described here: https://url.spec.whatwg.org/#url-rendering
    [style setBaseWritingDirection:NSWritingDirectionLeftToRight];
    [style setLineBreakMode:NSLineBreakByCharWrapping];

    [mutableText addAttribute:NSParagraphStyleAttributeName
                        value:style
                        range:entireString];
  }

  [super setAttributedText:mutableText];
}

- (void)setText:(NSString*)text {
  NSAttributedString* as = [[NSAttributedString alloc] initWithString:text];
  [self setTextInternal:as autocompleteLength:0];
}

- (CGRect)caretRectForPosition:(UITextPosition*)position {
  // Hide the caret in pre-edit state or when showing added text (autocomplete
  // and/or additional text).
  if (self.isPreEditing || [self hasAddedText]) {
    return CGRectZero;
  }

  return [super caretRectForPosition:position];
}

- (NSArray<UITextSelectionRect*>*)selectionRectsForRange:(UITextRange*)range {
  // Hide the selection UI in pre-edit. UITextField is expected to hide the
  // selection UI when `clearsOnInsertion` is YES, but this behavior is not
  // working on iOS 17.
  if (self.isPreEditing) {
    return nil;
  }
  return [super selectionRectsForRange:range];
}

- (BOOL)hasText {
  // Returns YES when `allowsReturnKeyWithEmptyText` to enable the 'Go' key in
  // the keyboard.
  if (_forceDisableReturnKey) {
    return NO;
  }
  return self.allowsReturnKeyWithEmptyText || [super hasText];
}

#pragma mark - UITextInput

- (void)beginFloatingCursorAtPoint:(CGPoint)point {
  // Exit preedit because it blocks the view of the textfield.
  [self exitPreEditState];
  [super beginFloatingCursorAtPoint:point];
}

#pragma mark - UIView

- (UIView*)hitTest:(CGPoint)point withEvent:(UIEvent*)event {
  // Anything in the narrow bar above OmniboxTextFieldIOS view
  // will also activate the text field.
  if (point.y < 0) {
    point.y = 0;
  }
  return [super hitTest:point withEvent:event];
}

#pragma mark - UIGestureRecognizerDelegate

- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
    shouldRecognizeSimultaneouslyWithGestureRecognizer:
        (UIGestureRecognizer*)otherGestureRecognizer {
  // Prevent conflicts between the pan gesture from the UIScrollView and the
  // long press scroll gesture (crbug.com/457389541).
  if ([gestureRecognizer isKindOfClass:UIPanGestureRecognizer.class] &&
      [otherGestureRecognizer
          isKindOfClass:UILongPressGestureRecognizer.class]) {
    return NO;
  }
  return YES;
}

- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer*)gestureRecognizer {
  if (gestureRecognizer == _tapGestureRecognizer) {
    return [self isPreEditing] || [self hasAutocompleteText] ||
           [self hasAdditionalText];
  }
  return YES;
}

#pragma mark - UIResponder

// Triggered on tap gesture recognizer.
- (void)handleTap:(UITapGestureRecognizer*)sender {
  if (self.isPreEditing) {
    [self exitPreEditState];
    [super selectAll:self];
  }
  if (self.hasAutocompleteText) {
    [self acceptAutocompleteText];
  }
  if (self.hasAdditionalText) {
    [self handleUserInitiatedRemovalOfAdditionalText];
  }
}

- (void)select:(id)sender {
  if ([self isPreEditing]) {
    [self exitPreEditState];
  }
  [super select:sender];
}

- (void)selectAll:(id)sender {
  if ([self isPreEditing]) {
    [self exitPreEditState];
  }
  if ([self hasAutocompleteText]) {
    [self acceptAutocompleteText];
  }
  if ([self hasAdditionalText]) {
    [self handleUserInitiatedRemovalOfAdditionalText];
  }
  [super selectAll:sender];
}

- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
  // TODO(crbug.com/40280508): Improve this short term fix.
  if (action == @selector(undoManager)) {
    return YES;
  }

  // If the text is not empty and there is selected text, show copy and cut.
  if ([self textInRange:self.selectedTextRange].length > 0 &&
      (action == @selector(cut:) || action == @selector(copy:))) {
    return YES;
  }

  // If the text is not empty and there is no selected text, show select
  if (self.text.length > 0 &&
      [self textInRange:self.selectedTextRange].length == 0 &&
      action == @selector(select:)) {
    return YES;
  }

  // If selected text is les than the text length, show selectAll.
  if ([self textInRange:self.selectedTextRange].length != self.text.length &&
      action == @selector(selectAll:)) {
    return YES;
  }

  // If there is pasteboard content, show paste.
  if (_pasteboardHasStrings && action == @selector(paste:)) {
    return YES;
  }

  // Arrow keys are handled by OmniboxKeyboardDelegates, if they don't handle
  // them, default behavior of UITextField applies.
  if (action == @selector(forwardKeyCommandUp)) {
    return [self.omniboxKeyboardDelegate canPerformKeyboardAction:kUpArrow];
  } else if (action == @selector(forwardKeyCommandDown)) {
    return [self.omniboxKeyboardDelegate canPerformKeyboardAction:kDownArrow];
  } else if (action == @selector(forwardKeyCommandLeft)) {
    return [self.omniboxKeyboardDelegate canPerformKeyboardAction:kLeftArrow];
  } else if (action == @selector(forwardKeyCommandRight)) {
    return [self.omniboxKeyboardDelegate canPerformKeyboardAction:kRightArrow];
  }

  if (IsComposeboxPhysicalKeyboardReturnKeysEnabled()) {
    if (action == @selector(forwardKeyCommandShiftReturn:)) {
      return YES;
    }
  }

  // Handle pre-edit shortcuts.
  if ([self isPreEditing]) {
    // Allow cut and copy in preedit.
    if ((action == @selector(copy:)) || (action == @selector(cut:))) {
      return YES;
    }
  }

  // Note that this NO does not keep other elements in the responder chain from
  // adding actions they handle to the menu.
  return NO;
}

#pragma mark Copy/Paste

// Overridden to allow for custom omnibox copy behavior.  This includes
// preprending http:// to the copied URL if needed.
- (void)copy:(id)sender {
  id<OmniboxTextInputDelegate> delegate = self.omniboxTextInputDelegate;

  // Must test for the onCopy method, since it's optional.
  if ([delegate respondsToSelector:@selector(textInputDidCopy:)]) {
    [delegate textInputDidCopy:self];
  } else {
    [super copy:sender];
  }
}

- (void)cut:(id)sender {
  if ([self isPreEditing]) {
    [self copy:sender];
    [self exitPreEditState];
    NSAttributedString* emptyString = [[NSAttributedString alloc] init];
    [self setText:emptyString userTextLength:0];
  } else {
    [super cut:sender];
  }
}

// Overridden to notify the delegate that a paste is in progress.
- (void)paste:(id)sender {
  id<OmniboxTextInputDelegate> delegate = self.omniboxTextInputDelegate;
  if ([delegate respondsToSelector:@selector(textInputWillPaste:)]) {
    [delegate textInputWillPaste:self];
  }
  [super paste:sender];
}

#pragma mark UIPasteConfigurationSupporting

// Used by UIPasteControl to check if can paste.
- (BOOL)canPasteItemProviders:(NSArray<NSItemProvider*>*)itemProviders {
  if ([self.omniboxTextInputDelegate
          respondsToSelector:@selector(textInput:canPasteItemProviders:)]) {
    return [self.omniboxTextInputDelegate textInput:self
                              canPasteItemProviders:itemProviders];
  } else {
    return NO;
  }
}

// Used by UIPasteControl to paste.
- (void)pasteItemProviders:(NSArray<NSItemProvider*>*)itemProviders {
  if ([self.omniboxTextInputDelegate
          respondsToSelector:@selector(textInput:pasteItemProviders:)]) {
    [self.omniboxTextInputDelegate textInput:self
                          pasteItemProviders:itemProviders];
  }
}

#pragma mark UIKeyInput

// Override deleteBackward so that backspace clear autocomplete text.
- (void)deleteBackward {
  if ([self hasAutocompleteText]) {
    [self clearAutocompleteText];
    return;
  }
  if ([self hasAdditionalText]) {
    [self handleUserInitiatedRemovalOfAdditionalText];
    return;
  }
  // Must test for the onDeleteBackward method, since it's optional.
  if ([self.omniboxTextInputDelegate
          respondsToSelector:@selector(textInputDidDeleteBackward:)]) {
    [self.omniboxTextInputDelegate textInputDidDeleteBackward:self];
  }
  [super deleteBackward];
}

#pragma mark Key Command Forwarding

- (void)forwardKeyCommandUp {
  [self.omniboxKeyboardDelegate performKeyboardAction:kUpArrow];
}

- (void)forwardKeyCommandDown {
  [self.omniboxKeyboardDelegate performKeyboardAction:kDownArrow];
}

- (void)forwardKeyCommandLeft {
  [self.omniboxKeyboardDelegate performKeyboardAction:kLeftArrow];
}

- (void)forwardKeyCommandRight {
  [self.omniboxKeyboardDelegate performKeyboardAction:kRightArrow];
}

- (void)forwardKeyCommandShiftReturn:(UIKeyCommand*)command {
  base::RecordAction(
      base::UserMetricsAction("IOS.Omnibox.PhysicalKeyboardShiftReturn"));
  _insertingNewline = YES;
  [self insertText:@"\n"];
  _insertingNewline = NO;
}

// Arrow keys are forwarded to the main OmniboxKeyboardDelegate that will
// dispatch them to OmniboxPopupViewController or OmniboxViewController, if they
// don't handle them, default behavior of UITextField applies.
// Return keys are overridden in composebox and cobrowse to send on
// return/cmd+return, and add new line on shift+return.
- (NSArray<UIKeyCommand*>*)keyCommands {
  UIKeyCommand* commandUp =
      [UIKeyCommand keyCommandWithInput:UIKeyInputUpArrow
                          modifierFlags:0
                                 action:@selector(forwardKeyCommandUp)];
  UIKeyCommand* commandDown =
      [UIKeyCommand keyCommandWithInput:UIKeyInputDownArrow
                          modifierFlags:0
                                 action:@selector(forwardKeyCommandDown)];
  UIKeyCommand* commandLeft =
      [UIKeyCommand keyCommandWithInput:UIKeyInputLeftArrow
                          modifierFlags:0
                                 action:@selector(forwardKeyCommandLeft)];
  UIKeyCommand* commandRight =
      [UIKeyCommand keyCommandWithInput:UIKeyInputRightArrow
                          modifierFlags:0
                                 action:@selector(forwardKeyCommandRight)];

  commandUp.wantsPriorityOverSystemBehavior = YES;
  commandDown.wantsPriorityOverSystemBehavior = YES;
  commandLeft.wantsPriorityOverSystemBehavior = YES;
  commandRight.wantsPriorityOverSystemBehavior = YES;

  NSMutableArray<UIKeyCommand*>* commands = [NSMutableArray
      arrayWithObjects:commandUp, commandDown, commandLeft, commandRight, nil];

  if (IsComposeboxPhysicalKeyboardReturnKeysEnabled() &&
      (_presentationContext == OmniboxPresentationContext::kComposebox ||
       _presentationContext == OmniboxPresentationContext::kCobrowse)) {
    UIKeyCommand* commandShiftReturn = [UIKeyCommand
        keyCommandWithInput:@"\r"
              modifierFlags:UIKeyModifierShift
                     action:@selector(forwardKeyCommandShiftReturn:)];

    commandShiftReturn.wantsPriorityOverSystemBehavior = YES;

    [commands addObject:commandShiftReturn];
  }

  return commands;
}

#pragma mark - UIAccessibilityElement

- (NSString*)accessibilityValue {
  if (self.text.length == 0) {
    return self.placeholderLabel.text;
  }
  return self.text;
}

#pragma mark - OmniboxKeyboardDelegate

- (BOOL)canPerformKeyboardAction:(OmniboxKeyboardAction)keyboardAction {
  switch (keyboardAction) {
      // These up/down arrow key commands override the standard UITextInput
      // handling of up/down arrow key. The standard behavior is to go to the
      // beginning/end of the text. Remove this behavior to avoid inconsistent
      // behavior when popup can and cannot move up and down.
    case kUpArrow:
    case kDownArrow:
      return YES;
      // React to left and right keys when in preedit state to exit preedit and
      // put cursor to the beginning/end of the textfield; or if there is inline
      // suggestion displayed, accept it and put the cursor before/after the
      // suggested text.
    case kLeftArrow:
    case kRightArrow:
      return ([self isPreEditing] || [self hasAutocompleteText] ||
              [self hasAdditionalText]);
    case kReturnKey: {
      if (_forceDisableReturnKey) {
        return NO;
      }
      NSString* trimmedText =
          [self.text stringByTrimmingCharactersInSet:
                         [NSCharacterSet whitespaceAndNewlineCharacterSet]];
      // Don't allow return when the text is empty. (crbug.com/400861280)
      return self.allowsReturnKeyWithEmptyText || trimmedText.length;
    }
  }
}

- (void)performKeyboardAction:(OmniboxKeyboardAction)keyboardAction {
  DCHECK([self canPerformKeyboardAction:keyboardAction]);
  switch (keyboardAction) {
    case kUpArrow:
    case kDownArrow:
      // Up and down arrow do nothing instead of standard behavior. The standard
      // behavior is to go to the beginning/end of the text.
      break;
    case kLeftArrow:
      [self keyCommandLeft];
      break;
    case kRightArrow:
      [self keyCommandRight];
      break;
    case kReturnKey:
      [self.omniboxTextInputDelegate textInputDidAcceptInput:self];
      break;
  }
}

#pragma mark preedit and inline autocomplete key commands

- (void)keyCommandLeft {
  CHECK([self isPreEditing] || [self hasAutocompleteText] ||
        [self hasAdditionalText]);

  // Cursor offset.
  NSInteger offset = 0;
  if ([self isPreEditing]) {
    [self exitPreEditState];
  }

  const BOOL hasAutocompleteText = [self hasAutocompleteText];
  if (hasAutocompleteText) {
    // The cursor should stay in the end of the user input.
    offset = self.userText.length;

    // Accept autocomplete suggestion.
    [self acceptAutocompleteText];
  }
  if ([self hasAdditionalText]) {
    if (!hasAutocompleteText) {
      offset = self.userText.length - 1;
    }
    [self handleUserInitiatedRemovalOfAdditionalText];
  }

  // Place the cursor at computed offset.
  UITextPosition* beginning = self.beginningOfDocument;
  UITextPosition* cursorPosition = [self positionFromPosition:beginning
                                                       offset:offset];
  UITextRange* textRange = [self textRangeFromPosition:cursorPosition
                                            toPosition:cursorPosition];
  self.selectedTextRange = textRange;
}

- (void)keyCommandRight {
  CHECK([self isPreEditing] || [self hasAutocompleteText] ||
        [self hasAdditionalText]);

  if ([self isPreEditing]) {
    [self exitPreEditState];
  }

  if ([self hasAutocompleteText]) {
    [self acceptAutocompleteText];
  }
  if ([self hasAdditionalText]) {
    [self handleUserInitiatedRemovalOfAdditionalText];
  }

  // Put the cursor to the end of the input.
  UITextPosition* end = self.endOfDocument;
  UITextRange* textRange = [self textRangeFromPosition:end toPosition:end];

  self.selectedTextRange = textRange;
}

#pragma mark - Private methods

- (void)updatePlaceholder {
  if (_customPlaceholderText) {
    self.placeholderLabel.text = _customPlaceholderText;
    return;
  }

  self.placeholderLabel.text = _defaultPlaceholderText;
}

#pragma mark Font

/// Font to use in regular x regular size class. If not set, the regular font is
/// used instead.
- (UIFont*)largerFont {
  return PreferredFontForTextStyleWithMaxCategory(
      UIFontTextStyleBody, self.traitCollection.preferredContentSizeCategory,
      UIContentSizeCategoryAccessibilityExtraLarge);
}

/// Font to use in Compact x Any and Any x Compact size class.
- (UIFont*)normalFont {
  return PreferredFontForTextStyleWithMaxCategory(
      UIFontTextStyleSubheadline,
      self.traitCollection.preferredContentSizeCategory,
      UIContentSizeCategoryAccessibilityExtraLarge);
}

/// Font that should be used in current size class.
- (UIFont*)currentFont {
  return IsCompactWidth(self) ? self.normalFont : self.largerFont;
}

/// Updates the omnibox typing attributes with the current font.
- (void)updateOmniboxTypingAttributes {
  NSMutableDictionary<NSAttributedStringKey, id>* attributes =
      self.typingAttributes.mutableCopy;
  [attributes setValue:self.currentFont forKey:NSFontAttributeName];
  [attributes setValue:UIColor.clearColor
                forKey:NSBackgroundColorAttributeName];
  [attributes setValue:_defaultTextColor forKey:NSForegroundColorAttributeName];
  _omniboxTypingAttributes = attributes;
}

#pragma mark Helpers

/// Length of added text in the omnibox (autocomplete and additional text).
- (NSUInteger)addedTextLength {
  return _autocompleteTextLength + self.attributedAdditionalText.length;
}

/// Returns whether there is added text in the omnibox (autocomplete or
/// additional text).
- (BOOL)hasAddedText {
  return [self addedTextLength] > 0;
}

/// Returns whether there is additional text.
- (BOOL)hasAdditionalText {
  return self.attributedAdditionalText.length;
}

/// Text in the omnibox without the additional text.
- (NSAttributedString*)textWithoutAdditionalText {
  if (!self.attributedAdditionalText.length) {
    return self.attributedText;
  }

  CHECK_LE(self.attributedAdditionalText.length, self.attributedText.length,
           base::NotFatalUntil::M160);
  /// This should not happen, tracking occurences with NotFatalUntil
  /// crbug.com/421229993.
  if (self.attributedText.length < self.attributedAdditionalText.length) {
    return self.attributedText;
  }

  NSUInteger textLength =
      self.attributedText.length - self.attributedAdditionalText.length;
  NSAttributedString* substring = [self.attributedText
      attributedSubstringFromRange:NSMakeRange(0, textLength)];
  return substring;
}

/// Removes the additional text.
- (void)removeAdditionalText {
  if (!self.attributedAdditionalText) {
    return;
  }
  NSAttributedString* substring = [self textWithoutAdditionalText];
  _attributedAdditionalText = nil;
  [self setTextInternal:substring autocompleteLength:_autocompleteTextLength];
}

/// Removes the additional text and calls the delegate to update the
/// suggestions.
- (void)handleUserInitiatedRemovalOfAdditionalText {
  [self removeAdditionalText];
  if ([self.omniboxTextInputDelegate
          respondsToSelector:@selector(textInputDidRemoveAdditionalText:)]) {
    [self.omniboxTextInputDelegate textInputDidRemoveAdditionalText:self];
  }
}

/// Accepts the autocomplete text.
- (void)acceptAutocompleteText {
  [self setText:[self textWithoutAdditionalText].string];
  if ([self.omniboxTextInputDelegate
          respondsToSelector:@selector(textInputDidAcceptAutocomplete:)]) {
    [self.omniboxTextInputDelegate textInputDidAcceptAutocomplete:self];
  }
}

/// Sets the `text` in the textfield. `text` includes autocomplete text but
/// doesn't include the additional text. The additional text is taken from
/// `self.attributedAdditionalText`.
- (void)setTextInternal:(NSAttributedString*)text
     autocompleteLength:(NSUInteger)autocompleteLength {
  _autocompleteTextLength = autocompleteLength;
  // Extract substrings for the permanent text and the autocomplete text.  The
  // former needs to retain any text attributes from the original string.
  NSUInteger beginningOfAutocomplete = text.length - autocompleteLength;
  NSRange userTextRange = NSMakeRange(0, beginningOfAutocomplete);

  NSMutableAttributedString* fieldText =
      [[text attributedSubstringFromRange:userTextRange] mutableCopy];
  [fieldText addAttributes:_omniboxTypingAttributes range:userTextRange];

  if (autocompleteLength > 0) {
    // Creating `autocompleteText` from `[text string]` has the added bonus of
    // removing all the previously set attributes. This way the autocomplete
    // text doesn't have a highlighted protocol, etc.
    NSMutableAttributedString* autocompleteText =
        [[NSMutableAttributedString alloc]
            initWithString:[text.string
                               substringFromIndex:beginningOfAutocomplete]];

    [autocompleteText addAttribute:NSBackgroundColorAttributeName
                             value:self.selectedTextBackgroundColor
                             range:NSMakeRange(0, autocompleteLength)];
    [fieldText appendAttributedString:autocompleteText];
  }
  // Append additional text.
  if (self.attributedAdditionalText) {
    [fieldText appendAttributedString:self.attributedAdditionalText];
  }

  // The following BOOL was introduced to workaround a UIKit bug
  // (crbug.com/737589, rdar/32817402). The bug relates to third party keyboards
  // that check the value of textDocumentProxy.documentContextBeforeInput to
  // show keyboard suggestions. It appears that calling setAttributedText during
  // an EditingChanged UIControlEvent somehow triggers this bug. The reason we
  // update the attributed text here is to change the colors of the omnibox
  // (such as host, protocol) when !self.editing, but also to hide real
  // UITextField text under the _selection text when self.editing. Since we will
  // correct the omnibox editing text color anytime `self.text` is different
  // than `fieldText`, it seems it's OK to skip calling self.attributedText
  // during the condition added below. If we change mobile omnibox to match
  // desktop and also color the omnibox while self.editing, this workaround will
  // no longer work. The check for `autocompleteLength` reduces the scope of
  // this workaround, without it having introduced crbug.com/740075.
  BOOL updateText = YES;
  if (experimental_flags::IsThirdPartyKeyboardWorkaroundEnabled()) {
    updateText =
        (!self.editing || ![self.text isEqualToString:fieldText.string] ||
         autocompleteLength == 0);
  }
  if (updateText) {
    self.attributedText = fieldText;

    // TODO(crbug.com/330964534): Remove DUMP_WILL_BE_CHECK after investigating
    // crash.
    if (!self.endOfDocument || !self.beginningOfDocument) {
      DUMP_WILL_BE_NOTREACHED()
          << "autocomplete length: " << autocompleteLength
          << " text length: " << text.length << " has text position: "
          << (self.beginningOfDocument || self.endOfDocument);
    } else {
      UITextPosition* endOfUserText =
          [self positionFromPosition:self.beginningOfDocument
                              offset:beginningOfAutocomplete];
      // Move the cursor to the beginning of the field before setting the
      // position to the end of the user input so if the text is very wide, the
      // user sees the beginning of the text instead of the end.
      self.selectedTextRange =
          [self textRangeFromPosition:self.beginningOfDocument
                           toPosition:self.beginningOfDocument];
      // Preserve the cursor position at the end of the user input.
      self.selectedTextRange = [self textRangeFromPosition:endOfUserText
                                                toPosition:endOfUserText];
    }
  }

  // iOS changes the font to .LastResort when some unexpected unicode strings
  // are used (e.g. 𝗲𝗺𝗽𝗵𝗮𝘀𝗶𝘀).  Setting the NSFontAttributeName in the
  // attributed string to -systemFontOfSize fixes part of the problem, but the
  // baseline changes so text is out of alignment.
  [self setFont:self.currentFont];
  [self updateTextDirection];
  [self updateUIForCurrentText];
}

/// Returns the background color for selected text.
- (UIColor*)selectedTextBackgroundColor {
  return [self.tintColor colorWithAlphaComponent:0.2];
}

- (void)pasteboardDidChange:(NSNotification*)notification {
  __weak __typeof(self) weakSelf = self;
  GetGeneralPasteboard(base::BindOnce(^(UIPasteboard* pasteboard) {
    [weakSelf pasteboardDidChangeCallback:pasteboard];
  }));
}

- (void)pasteboardDidChangeCallback:(UIPasteboard*)pasteboard {
  _pasteboardHasStrings = pasteboard.hasStrings;
}

- (void)applicationDidBecomeActive:(NSNotification*)notification {
  [self pasteboardDidChange:nil];
}

// Resets Omnibox's the font and attributed text when a UITrait is modified.
- (void)updateTextProperitesOnTraitChange {
  // Reset the fonts to the appropriate ones in this size class.
  self.font = self.currentFont;
  _cachedSingleLineHeight = 0;
  [self updateTextContainerInset];
  self.placeholderLabel.font = self.font;
  [self setAttributedText:self.attributedText];
  [self updateOmniboxTypingAttributes];
  [self.heightDelegate textViewContentChanged:self];
}

- (void)updateTextContainerInset {
  BOOL isComposeboxIpad =
      IsComposeboxIpadEnabled() &&
      ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_PHONE;
  CGFloat minVerticalInset =
      isComposeboxIpad ? kOmniboxTextViewMinVerticalInsetIPadComposebox
                       : kOmniboxTextViewMinVerticalInset;
  if (self.minimumHeight <= 0) {
    // Reset to default values.
    self.textContainerInset =
        UIEdgeInsetsMake(minVerticalInset, 0, minVerticalInset, 0);
    _placeholderTopConstraint.constant = minVerticalInset;
    return;
  }
  CGFloat lineHeight = [self singleLineHeight];
  CGFloat minHeight = self.minimumHeight;
  CGFloat verticalPadding =
      MAX(minVerticalInset * 2.0, (minHeight - lineHeight));
  // Distribute padding.
  CGFloat verticalOffset =
      IsRegularXRegularSizeClass(self.traitCollection) ? 0.0 : kVerticalOffset;
  CGFloat topPadding = verticalPadding / 2.0 + verticalOffset;
  CGFloat bottomPadding = verticalPadding - topPadding;
  self.textContainerInset = UIEdgeInsetsMake(topPadding, 0, bottomPadding, 0);
  _placeholderTopConstraint.constant = topPadding;
}

/// Returns the height of a single line of text with the current font.
- (CGFloat)singleLineHeight {
  if (_cachedSingleLineHeight > 0) {
    return _cachedSingleLineHeight;
  }
  UIFont* font = self.font ?: self.currentFont;
  // Create a sample attributed string for one line.
  NSAttributedString* singleLineSampler =
      [[NSAttributedString alloc] initWithString:@"T"
                                      attributes:@{NSFontAttributeName : font}];
  CGSize singleLineConstraint = CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX);
  NSStringDrawingOptions options =
      NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading;
  CGRect singleLineBoundingRect =
      [singleLineSampler boundingRectWithSize:singleLineConstraint
                                      options:options
                                      context:nil];
  CGFloat measuredSingleLineHeight = ceilf(singleLineBoundingRect.size.height);

  // If for some reason measurement fails, fall back to font.lineHeight.
  if (measuredSingleLineHeight <= 0) {
    measuredSingleLineHeight = font.lineHeight;
  }
  _cachedSingleLineHeight = measuredSingleLineHeight;
  return measuredSingleLineHeight;
}

- (void)updatePlaceholderVisibility {
  self.placeholderLabel.hidden = self.text.length > 0;
}

#pragma mark - OmniboxTextInput

- (UIView*)view {
  return self;
}

- (UIView*)viewForVerticalAlignment {
  return self.placeholderLabel ? self.placeholderLabel : self;
}

- (UIResponder<UITextInput>*)scribbleInput {
  return self;
}

- (NSString*)placeholder {
  return self.placeholderLabel.text;
}

- (void)forceDisableReturnKey:(BOOL)forceDisable {
  _forceDisableReturnKey = forceDisable;
  [self reloadInputViews];
}

- (void)setDefaultPlaceholderText:(NSString*)defaultPlaceholderText {
  _defaultPlaceholderText = [defaultPlaceholderText copy];
  [self updatePlaceholder];
}

- (void)setCustomPlaceholderText:(NSString*)customPlaceholderText {
  _customPlaceholderText = [customPlaceholderText copy];
  [self updatePlaceholder];
}

- (NSString*)textValueForTesting {
  CHECK_IS_TEST();
  return
      [NSString stringWithFormat:@"%@||||%@||||%@", self.userText ?: @"",
                                 self.autocompleteText ?: @"",
                                 self.attributedAdditionalText.string ?: @""];
}

#pragma mark - UITextViewDelegate

- (void)textViewDidChange:(UITextView*)textView {
  [self.omniboxTextInputDelegate textInputDidChange:self];
  [self updateUIForCurrentText];
}

- (void)textViewDidChangeSelection:(UITextView*)textView {
  // UITextView resets typing attributes on text selection change, reapply them
  // here.
  self.typingAttributes = _omniboxTypingAttributes;
}

- (BOOL)textView:(UITextView*)textView
    shouldChangeTextInRange:(NSRange)range
            replacementText:(NSString*)text {
  // Only check for characters sent by the Return/Enter key (\n and \r) to
  // trigger submission. Other unicode line breaks (e.g. pasted paragraph
  // separators) should be allowed to be inserted as text.
  BOOL isNewline = [text isEqualToString:@"\n"] || [text isEqualToString:@"\r"];
  BOOL isCobrowse =
      _presentationContext == OmniboxPresentationContext::kCobrowse;
  if (isNewline && !_insertingNewline && !isCobrowse) {
    return [self.omniboxTextInputDelegate textInputShouldReturn:self];
  }
  return [self.omniboxTextInputDelegate textInput:self
                          shouldChangeTextInRange:range
                                replacementString:text];
}

- (void)textViewDidBeginEditing:(UITextView*)textView {
  _editing = YES;
  [self updateOmniboxTypingAttributes];
  [self.omniboxTextInputDelegate textInputDidBeginEditing:self];
}

- (void)textViewDidEndEditing:(UITextView*)textView {
  [self.omniboxTextInputDelegate textInputDidEndEditing:self];
  _editing = NO;
}

- (UIMenu*)textView:(UITextView*)textView
    editMenuForTextInRange:(NSRange)range
          suggestedActions:(NSArray<UIMenuElement*>*)suggestedActions {
  return [self.omniboxTextInputDelegate textInput:self
                     editMenuForCharactersInRange:range
                                 suggestedActions:suggestedActions];
}

#pragma mark - Private

/// Updates the UI for the current text without triggering autocomplete.
- (void)updateUIForCurrentText {
  [self updatePlaceholderVisibility];
  [self.heightDelegate textViewContentChanged:self];
  self.typingAttributes = _omniboxTypingAttributes;
  [self.omniboxTextInputDelegate textInputDidUpdateUIForText:self];
}

@end
