///|
/// Readonly editor model identity plus immutable text snapshot, plus the
/// Monaco decoration fields (the one mutable part; the decoration API lives
/// in `text_model_decorations.mbt`).
const LARGE_FILE_SIZE_THRESHOLD : Int32 = 20 * 1024 * 1024

///|
const LARGE_FILE_LINE_COUNT_THRESHOLD : Int32 = 300 * 1000

///|
pub struct TextModel {
  uri : @base_common.Uri
  display_name : String
  language_id : String
  version : Int32
  revision : String
  /// Mutable since the `set_value` flush lane: swapped whole
  /// (`_setValueFromTextBuffer`'s `this._buffer = textBuffer`,
  /// `textModel.ts:521`), never edited in place.
  mut snapshot : TextSnapshot
  /// The `model.tokenization` part (Monaco's `TokenizationTextModelPart`,
  /// wired in the `TextModel` constructor exactly as `textModel.ts` does).
  tokenization : TokenizationTextModelPart
  /// TextModel owns the attached-view aggregate; tokenization only borrows it.
  priv attached_views : AttachedViews
  /// Construction-safe live facts shared with the model-owned tokenization part.
  priv tokenization_state : TokenizationModelState
  /// `_onDidChangeAttached` (`textModel.ts:235`), owned and released by the
  /// model rather than by the borrowed attached-view aggregate.
  priv attached_change_emitter : @base_common.Emitter[Unit]
  // Lifecycle (`_onWillDispose` textModel.ts:222, `_isDisposed` :190,
  // `__isDisposing` :260)
  on_will_dispose : @base_common.Emitter[Unit]
  priv mut is_disposed : Bool
  /// `_versionId` (`textModel.ts:262`, init 1 at `:370`): bumped by the
  /// content lane (`_increaseVersionId`). Together with the stable model
  /// identity this is async freshness authority; the host-facing `version`
  /// is caller-owned metadata.
  priv mut version_id : Int32
  // Content events (`_eventEmitter` textModel.ts:244; `_viewModels` :303)
  priv event_emitter : DidChangeContentEmitter
  priv view_models : Array[ViewModelCallbacks]
  // #region Decorations (textModel.ts:285-290; `_onDidChangeDecorations` :225)
  /// Full process-local identity used for exact model-instance comparisons.
  priv identity : Int32
  /// Stable decoration-id namespace prefix derived from `identity`.
  priv decoration_id_namespace : String
  priv mut delta_decoration_call_cnt : Int32
  priv mut last_decoration_id : Int32
  /// Mutable since the `set_value` flush lane: replaced wholesale
  /// ("Destroy all my decorations", `textModel.ts:526-528`).
  priv mut decorations : Map[String, IntervalNode]
  priv mut decorations_tree : DecorationsTrees
  on_did_change_decorations : DidChangeDecorationsEmitter
  // #endregion
}

///|
pub impl Debug for TextModel with fn to_repr(self) {
  Repr(
    "",
  )
}

///|
/// Whether two handles name the same stable text-model instance.
pub fn TextModel::same(self : TextModel, other : TextModel) -> Bool {
  self.identity == other.identity
}

///|
/// Stable process-local key for identity-indexed package services.
///
/// Most callers should prefer `same`; this numeric form exists for maps that
/// cannot key directly by `TextModel` without falling back to reference
/// equality.
pub fn TextModel::identity(self : TextModel) -> Int32 {
  self.identity
}

///|
/// Builds a readonly text model.
///
/// `version` and `revision` are caller-owned metadata, not async freshness
/// keys. Request owners use stable model identity plus `get_version_id()`.
/// Empty display name and language id are normalized; non-positive versions
/// become version 1.
pub fn TextModel::TextModel(
  uri : @base_common.Uri,
  display_name : String,
  language_id : String,
  version : Int32,
  revision : String,
  text : String,
) -> TextModel {
  let model = new_text_model(
    uri,
    display_name,
    language_id,
    version,
    revision,
    text,
    None,
    None,
  )
  offer_text_model_browser_diagnostic(model => model.get_attached_editor_count())
  model
}

///|
/// Shared production/test constructor. Production passes no resolver and
/// therefore uses the registry-backed token part; the package test seam can
/// inject a failing source-shaped adapter while retaining the exact TextModel
/// ownership, reporting, and lifecycle path.
fn new_text_model(
  uri : @base_common.Uri,
  display_name : String,
  language_id : String,
  version : Int32,
  revision : String,
  text : String,
  support_resolver : ((String) -> InternalTokenizationSupportAdapter?)?,
  unexpected_error_observer : ((String) -> Unit)?,
) -> TextModel {
  let display_name = if display_name == "" {
    uri.to_string()
  } else {
    display_name
  }
  let language_id = if language_id == "" { "moonbit" } else { language_id }
  let version = version.max(1)
  let snapshot = TextSnapshot(text)
  // Permanently respect the construction-time large-file decision. Equality
  // stays enabled; only strict greater-than disables tokenization.
  let is_too_large_for_tokenization = snapshot.get_length() >
    LARGE_FILE_SIZE_THRESHOLD ||
    snapshot.line_count() > LARGE_FILE_LINE_COUNT_THRESHOLD
  let model_state = TokenizationModelState(
    snapshot=Ref(snapshot),
    language_id~,
    attached_count=Ref(0),
    too_large=is_too_large_for_tokenization,
    disposing=Ref(false),
    scheduler=Ref(None),
    report_unexpected_error=message => {
      // This package is deliberately host-neutral. `println` reaches the JS
      // console and native stderr/stdout plumbing without importing browser or
      // shell effects, matching the established decoration warning seam.
      println("[viewer/tokenization] unexpected tokenizer error: \{message}")
      match unexpected_error_observer {
        Some(observer) => observer(message)
        None => ()
      }
    },
  )
  let attached_views = AttachedViews()
  let tokenization = match support_resolver {
    Some(resolve_support) =>
      TokenizationTextModelPart::new_with_support_resolver(
        model_state, attached_views, resolve_support,
      )
    None => TokenizationTextModelPart(model_state, attached_views)
  }
  let (identity, decoration_id_namespace) = next_model_identity()
  {
    uri,
    display_name,
    language_id,
    version,
    revision,
    snapshot,
    tokenization,
    attached_views,
    tokenization_state: model_state,
    attached_change_emitter: Emitter(),
    on_will_dispose: Emitter(),
    is_disposed: false,
    version_id: 1,
    event_emitter: DidChangeContentEmitter(),
    view_models: [],
    identity,
    decoration_id_namespace,
    delta_decoration_call_cnt: 0,
    last_decoration_id: 0,
    decorations: Map([]),
    decorations_tree: DecorationsTrees(),
    on_did_change_decorations: DidChangeDecorationsEmitter(),
  }
}

///|
/// Releases the model — `dispose` (`textModel.ts:414`). Fires `onWillDispose`
/// first (the attached editor detaches itself there), then disposes the
/// tokenization part — dropping its registry subscription — and the emitters.
/// Monaco's disposed-buffer swap is N-A: the snapshot is immutable and
/// garbage-collected.
pub fn TextModel::dispose(self : TextModel) -> Unit {
  run_text_model_dispose_sequence(
    set_disposing=value => self.tokenization_state.disposing.val = value,
    fire_will_dispose=() => self.on_will_dispose.fire(()),
    dispose_tokenization=() => self.tokenization.dispose(),
    set_disposed=() => self.is_disposed = true,
    dispose_registered_resources=() => {
      self.attached_views.dispose()
      self.tokenization_state.scheduler.val = None
      self.attached_change_emitter.dispose()
      self.on_will_dispose.dispose()
      self.on_did_change_decorations.actual.dispose()
      self.event_emitter.emitter.dispose()
    },
  )
}

///|
/// `TextModel.dispose` (`textModel.ts:414-421`) retained as one directly
/// testable source-order sequence. In particular, the terminal flag is set
/// after the token part is released but before superclass/registered resources.
fn run_text_model_dispose_sequence(
  set_disposing~ : (Bool) -> Unit,
  fire_will_dispose~ : () -> Unit,
  dispose_tokenization~ : () -> Unit,
  set_disposed~ : () -> Unit,
  dispose_registered_resources~ : () -> Unit,
) -> Unit {
  // `__isDisposing = true` before the will-dispose fire (`textModel.ts:415`);
  // `_emitContentChangedEvent` early-returns while set so no listener is
  // confused by an event after disposing (`:468-471`).
  set_disposing(true)
  fire_will_dispose()
  dispose_tokenization()
  set_disposed()
  dispose_registered_resources()
  set_disposing(false)
}

///|
/// Whether `dispose` has run — `isDisposed` (`textModel.ts:445`).
pub fn TextModel::is_disposed(self : TextModel) -> Bool {
  self.is_disposed
}

///|
/// Number of currently attached Viewer instances.
fn TextModel::get_attached_editor_count(self : TextModel) -> Int32 {
  self.tokenization_state.attached_count.val
}

///|
/// Begins one Viewer attachment (`textModel.ts:607-614`). The first non-None
/// scheduler of an attached epoch wins. A late first scheduler wakes existing
/// background work once without synthesizing another 0->1 notification.
pub fn TextModel::on_before_attached(
  self : TextModel,
  scheduler? : TokenizationScheduler,
) -> AttachedViewImpl {
  let mut installed_late_scheduler = false
  if self.tokenization_state.scheduler.val is None &&
    scheduler is Some(scheduler) {
    self.tokenization_state.scheduler.val = Some(scheduler)
    installed_late_scheduler = self.tokenization_state.attached_count.val > 0
  }
  if installed_late_scheduler {
    self.tokenization.handle_did_change_attached()
  }
  self.tokenization_state.attached_count.val += 1
  if self.tokenization_state.attached_count.val == 1 {
    self.tokenization.handle_did_change_attached()
    self.attached_change_emitter.fire(())
  }
  self.attached_views.attach_view()
}

///|
/// Finishes one Viewer attachment (`textModel.ts:616-623`). The final
/// transition notifies/cancels backend work first, detaches the exact handle
/// second, and clears the epoch scheduler last.
pub fn TextModel::on_before_detached(
  self : TextModel,
  attached_view : AttachedViewImpl,
) -> Unit {
  self.tokenization_state.attached_count.val -= 1
  if self.tokenization_state.attached_count.val == 0 {
    self.tokenization.handle_did_change_attached()
    self.attached_change_emitter.fire(())
  }
  self.attached_views.detach_view(attached_view)
  if self.tokenization_state.attached_count.val == 0 {
    self.tokenization_state.scheduler.val = None
  }
}

///|
/// Model listener ownership query (`textModel.ts:430-440`). The readonly
/// model carries will-dispose, decoration, token, attached, and content
/// emitters; unported options/line-height/font emitters are N-A. In
/// particular, the token-part substep must not be omitted.
fn TextModel::has_listeners(self : TextModel) -> Bool {
  self.on_will_dispose.has_listeners() ||
  self.on_did_change_decorations.actual.has_listeners() ||
  self.tokenization.has_listeners() ||
  self.attached_change_emitter.has_listeners() ||
  self.event_emitter.emitter.has_listeners()
}

///|
/// Keeps the package-internal source member in the non-test build; wbtests
/// exercise the token-listener ownership branch directly.
let _test_only_text_model_has_listeners_anchor : Unit = ignore(
  TextModel::has_listeners,
)

///|
/// Subscribes to the start of `dispose` — `onWillDispose` (`textModel.ts:223`).
pub fn TextModel::on_will_dispose(
  self : TextModel,
  listener : () -> Unit,
) -> @base_common.Disposable {
  self.on_will_dispose.event(_ => listener())
}

///|
/// Subscribes to grammar-token changes — `onDidChangeTokens`, delegating to
/// the tokenization part as the source's forwarded emitter does.
pub fn TextModel::on_did_change_tokens(
  self : TextModel,
  listener : (ModelTokensChangedEvent) -> Unit,
) -> @base_common.Disposable {
  self.tokenization.on_did_change_tokens(listener)
}

///|
/// Returns the immutable snapshot consumed by tokenization, providers, and
/// rendering.
pub fn TextModel::snapshot(self : TextModel) -> TextSnapshot {
  self.snapshot
}

// #region Reading text — the `textModel.ts` read section (:737-1176, word
// cluster :2085-2124), delegating to the snapshot buffer exactly as Monaco's
// members delegate to `this._buffer`. `_assertNotDisposed` guards are not
// ported (the immutable snapshot stays readable after `dispose`) and the
// viewer clamps where Monaco throws `BugIndicatingError`; see the README
// parity ledger.

///|
/// Source document language id. Mirrors `getLanguageId` (`textModel.ts:2085`),
/// delegating to the tokenization part as Monaco does.
pub fn TextModel::get_language_id(self : TextModel) -> String {
  self.tokenization.get_language_id()
}

///|
/// Clamped document offset of `position`. Mirrors `getOffsetAt`
/// (`textModel.ts:770`): relaxed-validate, then the buffer's line/column
/// arity.
pub fn TextModel::get_offset_at(
  self : TextModel,
  position : @base_common.Position,
) -> Int32 {
  let position = self.validate_position_impl(
    position.line_number,
    position.column,
    false,
  )
  self.snapshot.get_offset_at(position.line_number, position.column)
}

///|
/// Position at the clamped document `offset`. Mirrors `getPositionAt`
/// (`textModel.ts:776`).
pub fn TextModel::get_position_at(
  self : TextModel,
  offset : Int32,
) -> @base_common.Position {
  let offset = offset.clamp(min=0, max=self.snapshot.get_length())
  self.snapshot.get_position_at(offset)
}

///|
/// Full normalized document text. Mirrors `getValue` (`textModel.ts:799`):
/// derive the full model range, then read that range. U+FEFF is ordinary
/// Viewer content and there is no EOL or preserve-BOM selector.
pub fn TextModel::get_value(self : TextModel) -> String {
  self.get_value_in_range(self.get_full_model_range())
}

///|
/// Text inside a validated `range`. Mirrors `getValueInRange`
/// (`textModel.ts:831`): validate, then read the buffer.
pub fn TextModel::get_value_in_range(
  self : TextModel,
  range : @base_common.Range,
) -> String {
  self.snapshot.get_value_in_range(self.validate_range(range))
}

///|
/// Length, in UTF-16 code units, of `get_value_in_range`. Mirrors
/// `getValueLengthInRange` (`textModel.ts:836`).
fn TextModel::get_value_length_in_range(
  self : TextModel,
  range : @base_common.Range,
) -> Int32 {
  self.snapshot.get_value_length_in_range(self.validate_range(range))
}

///|
/// Number of logical lines. Mirrors `getLineCount` (`textModel.ts:846`).
pub fn TextModel::get_line_count(self : TextModel) -> Int32 {
  self.snapshot.line_count()
}

///|
/// Text of the one-based `line_number`, clamped into the document. Mirrors
/// `getLineContent` (`textModel.ts:851`).
pub fn TextModel::get_line_content(
  self : TextModel,
  line_number : Int32,
) -> String {
  self.snapshot.get_line_content(line_number)
}

///|
/// Every line's content, EOLs excluded. Mirrors `getLinesContent`
/// (`textModel.ts:869`).
pub fn TextModel::get_lines_content(self : TextModel) -> Array[String] {
  [
    for line in 0.. self.snapshot.line_text(line)
  ]
}

///|
/// Stored model end-of-line sequence. Mirrors `getEOL` (`textModel.ts:879`)
/// with the Viewer's approved fixed-LF representation.
fn TextModel::get_eol(self : TextModel) -> String {
  self.snapshot.get_eol()
}

///|
/// Minimum column of a line, always `1`. Mirrors `getLineMinColumn`
/// (`textModel.ts:892`).
pub fn TextModel::get_line_min_column(
  _self : TextModel,
  _line_number : Int32,
) -> Int32 {
  1
}

///|
/// Max (1-based) column for `line_number`: content length + 1. Mirrors
/// `getLineMaxColumn` (`textModel.ts:897`).
pub fn TextModel::get_line_max_column(
  self : TextModel,
  line_number : Int32,
) -> Int32 {
  self.snapshot.get_line_max_column(line_number)
}

///|
/// 1-based column of the first non-whitespace character on `line_number`, or
/// `0` for whitespace-only lines. Mirrors `getLineFirstNonWhitespaceColumn`
/// (`textModel.ts:905`).
pub fn TextModel::get_line_first_non_whitespace_column(
  self : TextModel,
  line_number : Int32,
) -> Int32 {
  self.snapshot.get_line_first_non_whitespace_column(line_number)
}

///|
/// 1-based column just past the last non-whitespace character on
/// `line_number`, or `0` for whitespace-only lines. Mirrors
/// `getLineLastNonWhitespaceColumn` (`textModel.ts:913`).
pub fn TextModel::get_line_last_non_whitespace_column(
  self : TextModel,
  line_number : Int32,
) -> Int32 {
  self.snapshot.get_line_last_non_whitespace_column(line_number)
}

///|
/// The whole document as a `Range`. Mirrors `getFullModelRange`
/// (`textModel.ts:1168`).
pub fn TextModel::get_full_model_range(self : TextModel) -> @base_common.Range {
  let line_count = self.get_line_count()
  Range(1, 1, line_count, self.get_line_max_column(line_count))
}

///|
/// Core of `validate_position` (Monaco's `_validatePosition`). When `surrogate`
/// is true, a column landing in the middle of a high-low surrogate pair is
/// moved to before the pair. `surrogate=false` is Monaco's `Relaxed` mode (a
/// plain clamp). Inputs are integers, so Monaco's `NaN`/float coercion is moot.
fn TextModel::validate_position_impl(
  self : TextModel,
  line_number : Int32,
  column : Int32,
  surrogate : Bool,
) -> @base_common.Position {
  let snapshot = self.snapshot
  let line_count = snapshot.line_count()
  if line_number < 1 {
    return Position(1, 1)
  }
  if line_number > line_count {
    return Position(line_count, snapshot.get_line_max_column(line_count))
  }
  if column <= 1 {
    return Position(line_number, 1)
  }
  let max_column = snapshot.get_line_max_column(line_number)
  if column >= max_column {
    return Position(line_number, max_column)
  }
  if surrogate {
    // column > 1 here; if it would split a high-low pair, move before it.
    let char_code_before = snapshot.get_line_char_code(line_number, column - 2)
    if @base_common.is_high_surrogate(char_code_before) {
      return Position(line_number, column - 1)
    }
  }
  Position(line_number, column)
}

///|
/// Clamps a position into the document, snapping out of the middle of a
/// surrogate pair. Mirrors `validatePosition` (`textModel.ts:1063`).
pub fn TextModel::validate_position(
  self : TextModel,
  position : @base_common.Position,
) -> @base_common.Position {
  self.validate_position_impl(position.line_number, position.column, true)
}

///|
/// Clamps a range into the document without surrogate snapping. Mirrors
/// `_validateRangeRelaxedNoAllocations` (`textModel.ts:925`) — the
/// decoration paths use this cheaper clamp instead of `validateRange`.
fn TextModel::validate_range_relaxed(
  self : TextModel,
  range : @base_common.Range,
) -> @base_common.Range {
  let start = self.validate_position_impl(
    range.start_line_number,
    range.start_column,
    false,
  )
  let end = self.validate_position_impl(
    range.end_line_number,
    range.end_column,
    false,
  )
  Range(start.line_number, start.column, end.line_number, end.column)
}

///|
/// Clamps a range into the document, expanding out of any surrogate pair its
/// ends land inside. Mirrors `validateRange` (`textModel.ts:1110`).
pub fn TextModel::validate_range(
  self : TextModel,
  range : @base_common.Range,
) -> @base_common.Range {
  let snapshot = self.snapshot
  let start = self.validate_position_impl(
    range.start_line_number,
    range.start_column,
    false,
  )
  let end = self.validate_position_impl(
    range.end_line_number,
    range.end_column,
    false,
  )
  let sl = start.line_number
  let sc = start.column
  let el = end.line_number
  let ec = end.column
  let char_before_start = if sc > 1 {
    snapshot.get_line_char_code(sl, sc - 2)
  } else {
    0
  }
  let char_before_end = if ec > 1 && ec <= snapshot.get_line_length(el) {
    snapshot.get_line_char_code(el, ec - 2)
  } else {
    0
  }
  let start_inside = @base_common.is_high_surrogate(char_before_start)
  let end_inside = @base_common.is_high_surrogate(char_before_end)
  if !start_inside && !end_inside {
    return Range(sl, sc, el, ec)
  }
  if sl == el && sc == ec {
    // do not expand a collapsed range, simply move it to a valid location
    return Range(sl, sc - 1, el, ec - 1)
  }
  if start_inside && end_inside {
    return Range(sl, sc - 1, el, ec + 1)
  }
  if start_inside {
    return Range(sl, sc - 1, el, ec)
  }
  Range(sl, sc, el, ec + 1)
}

///|
/// The current content version — `getVersionId` (`textModel.ts:737-740`);
/// init 1 (`:370`), bumped by every content change. The decoration trees
/// cache against it. `_assertNotDisposed`: N-A (readonly reads survive
/// dispose, see the region header).
pub fn TextModel::get_version_id(self : TextModel) -> Int32 {
  self.version_id
}

///|
/// `_increaseVersionId` (`textModel.ts:782-785`). The
/// `_alternativeVersionId` mirror is N-A (undo/redo not ported).
fn TextModel::increase_version_id(self : TextModel) -> Unit {
  self.version_id += 1
}

///|
/// Subscribes to content changes — `onDidChangeContent` (`textModel.ts:
/// 245-247`), unwrapping the internal event to its public payload.
pub fn TextModel::on_did_change_content(
  self : TextModel,
  listener : (ModelContentChangedEvent) -> Unit,
) -> @base_common.Disposable {
  self.event_emitter.event(e => listener(e.content_changed_event))
}

///|
/// Replaces the whole content in place — `setValue` (`textModel.ts:484-493`):
/// build the new buffer, then flush. The same model (and its registered view
/// models) survives; all decorations are destroyed and the version bumps.
/// The `null | undefined` argument guard is N-A (non-nullable `String`);
/// `EditSources.setValue()` reasons are not ported (event deviation).
pub fn TextModel::set_value(self : TextModel, value : String) -> Unit {
  // _assertNotDisposed: N-A (the immutable snapshot stays readable after
  // dispose; see the Reading region header).
  self.set_value_from_snapshot(TextSnapshot(value))
}

///|
/// `_createContentChanged2` (`textModel.ts:495-512`). `rangeEndPosition` is
/// accepted-and-unused in the source too; `detailedReasons` are not ported
/// (`text_model_events.mbt` header).
fn TextModel::create_content_changed_2(
  self : TextModel,
  range : @base_common.Range,
  range_offset : Int32,
  range_length : Int32,
  _range_end_position : @base_common.Position,
  text : String,
  is_undoing : Bool,
  is_redoing : Bool,
  is_flush : Bool,
  is_eol_change : Bool,
) -> ModelContentChangedEvent {
  {
    changes: [{ range, range_offset, range_length, text, }],
    // `this._buffer.getEOL()`: read from the newly installed snapshot.
    eol: self.get_eol(),
    is_eol_change,
    version_id: self.get_version_id(),
    is_undoing,
    is_redoing,
    is_flush,
  }
}

///|
/// `_setValueFromTextBuffer` (`textModel.ts:514-545`): capture the old full
/// range/length, swap the buffer, bump the version, destroy all decorations,
/// and emit `[ModelRawFlush]` plus the full-range public change
/// (`isFlush: true`). `_bufferDisposable` is N-A (GC); the undo-stack and
/// trim-whitespace clears (`:531-532`) are N-A (no edit stack).
fn TextModel::set_value_from_snapshot(
  self : TextModel,
  snapshot : TextSnapshot,
) -> Unit {
  let old_full_model_range = self.get_full_model_range()
  let old_model_value_length = self.get_value_length_in_range(
    old_full_model_range,
  )
  let end_line_number = self.get_line_count()
  let end_column = self.get_line_max_column(end_line_number)
  self.snapshot = snapshot
  // Synchronize shared live state before token content handling.
  self.tokenization_state.snapshot.val = snapshot
  self.increase_version_id()

  // Destroy all my decorations
  self.decorations = Map([])
  self.decorations_tree = DecorationsTrees()
  self.emit_content_changed_event(
    {
      changes: [ModelRawFlush],
      version_id: self.version_id,
      is_undoing: false,
      is_redoing: false,
    },
    self.create_content_changed_2(
      Range(1, 1, end_line_number, end_column),
      0,
      old_model_value_length,
      Position(end_line_number, end_column),
      self.get_value(),
      false,
      false,
      true,
      false,
    ),
  )
}

///|
/// `_emitContentChangedEvent` (`textModel.ts:467-482`). Call order is the
/// contract: tokenization part first, then the registered view models
/// (two-loop dispatch), then the public emitter. The bracket-pair and
/// font-token parts (`:473-474`) are N-A (not ported); `resultingSelection`
/// (`:476-479`) drops with the unported field (`text_model_events.mbt`).
fn TextModel::emit_content_changed_event(
  self : TextModel,
  raw_change : ModelRawContentChangedEvent,
  change : ModelContentChangedEvent,
) -> Unit {
  if self.tokenization_state.disposing.val {
    // Do not confuse listeners by emitting any event after disposing
    return
  }
  // The shared model state already sees the replacement snapshot. The part only
  // needs the flush fact (`tokenizationTextModelPart.ts:136-165`).
  self.tokenization.handle_did_change_content(is_flush=change.is_flush)
  let content_change_event : InternalModelContentChangeEvent = {
    raw_content_changed_event: raw_change,
    content_changed_event: change,
  }
  self.on_did_change_content_or_injected_text(content_change_event)
  self.event_emitter.fire(content_change_event)
}

// #endregion

///|
pub extend TextModel with Debug::{to_repr}