// Reachable subset of Monaco/VS Code's TextModel decoration cluster.
//
// Source: vscode/src/vs/editor/common/model/textModel.ts
// (checked-in vscode submodule):
// fields `:285-290` (carried on `TextModel`, `text_model.mbt`), event
// plumbing `:225-226,:1624-1667`, public API
// `:1669-1886`, private impls `:1890-2062`, `DecorationsTrees` `:2196-2353`,
// node predicates `:2180-2190`, `DidChangeDecorationsEmitter` `:2586-2666`.
//
// Deviations (plan `monaco-decoration-system-port.md`):
// - D5 (options interning): `register`/`createDynamic`/`_normalizeOptions`
//   collapse into the `ModelDecorationOptions::ModelDecorationOptions` constructor (in the source
//   both statics are the identical `new ModelDecorationOptions(options)`,
//   and `_normalizeOptions` is an `instanceof` test that has no analogue
//   with a single nominal options type).
// - D6 (model swap): decoration state is per-`TextModel` instance; the viewer
//   swaps whole models on version change, so owners re-attach on swap.
//   `set_value` bumps `_versionId` in place, but it also replaces the trees
//   wholesale ("Destroy all my decorations"), so each tree lifetime still
//   sees exactly one version in `cached_version_id`.
// - `changeDecorations`' `try/catch onUnexpectedError` around the callback is
//   unreachable (MoonBit callbacks here are non-raising), so the `T | null`
//   result is `T`.
// - `_assertNotDisposed`/`_isDisposed` guards are N-A (the readonly model has
//   no dispose lifecycle).
//
// Out-of-scope members (explicitly deferred/N-A, per plan):
// - Query helpers with no product caller (`getAllDecorations`,
//   `getOverviewRulerDecorations`, margin/injected-text interval queries, and
//   direct decoration-option lookup) are DEFERRED.
// - `DecorationsTrees.acceptReplace` and the model edit path: N-A — the
//   readonly viewer never edits a model (the interval tree's edit cluster is
//   not ported, see `interval_tree.mbt`'s header).
// - Injected-text per-line events: the `LineInjectedText` event type and
//   `recordLineAffectedByInjectedText` are DEFERRED with the whole-rebuild
//   deviation (consumers rebuild the projection on `affects_injected_text`;
//   the tree query surface itself is ported —
//   `get_injected_text_decorations`/`get_injected_text_in_interval`, the
//   latter subsuming `getLineInjectedText`'s tree query).
// - Line-height/font: `getCustomLineHeights*`, `getFontDecorations*`,
//   `handleBeforeFireDecorationsChangedEvent` fan-out,
//   `recordLineAffectedByLineHeightChange`/`FontChange`,
//   `_fireOnDidChangeLineHeight`/`Font` are N-A (uniform line height).
// - Bracket/font providers (`_decorationProvider`,
//   `_fontTokenDecorationsProvider`): getters return tree results only.
// - `_getTrackedRange`/`_setTrackedRange`/`TRACKED_RANGE_OPTIONS`: N-A —
//   editing-cursor internals with no consumer in the readonly viewer.
// - `DecorationsTrees.collectNodesPostOrder`: N-A — its only consumer is
//   TextModel.dispose.
// - `DecorationsTrees.ensureAllNodesHaveRanges`: N-A — its only consumer is
//   the `_onBeforeEOLChange` edit path.
// - `DidChangeDecorationsEmitter.hasListeners`: N-A — no consumer in the
//   ported slices.

///|
/// `DidChangeDecorationsEmitter::fire` (event-contract member with no product
/// caller) is consumed only by `text_model_decorations_wbtest.mbt`; anchor
/// it so the non-test build keeps it (the `moon explain 1` idiom).
let _test_only_member_anchor : Unit = ignore(DidChangeDecorationsEmitter::fire)

///|
/// `MODEL_ID` (`textModel.ts:123`): process-wide model counter. The full value
/// is both the stable MoonBit model identity and the decoration-id namespace.
/// This intentionally differs from Monaco's wrapped single-letter hash because
/// marker lookup exposes decoration ids across multiple same-URI models.
let model_identity_counter : Ref[Int32] = Ref(0)

///|
fn next_model_identity() -> (Int32, String) {
  model_identity_counter.val += 1
  let identity = model_identity_counter.val
  // Monaco's single-letter hash is only a compact decoration namespace; it
  // wraps every 52 models. This port also exposes decoration ids through a
  // same-URI marker lookup, so the namespace itself must stay unambiguous.
  (identity, identity.to_string())
}

// #region DidChangeDecorationsEmitter (textModel.ts:2586-2666)

///|
/// `DidChangeDecorationsEmitter` (`textModel.ts:2586`): batches
/// decoration-change notifications into one event per deferred-emit bracket,
/// accumulating the affects flags. The injected-text/line-height/font record
/// sets and the `handleBeforeFire` fan-out are dropped with their
/// out-of-scope features (see file header).
struct DidChangeDecorationsEmitter {
  actual : @base_common.Emitter[ModelDecorationsChangedEvent]
  mut deferred_cnt : Int32
  mut should_fire_deferred : Bool
  mut affects_minimap : Bool
  mut affects_overview_ruler : Bool
  mut affects_glyph_margin : Bool
  /// Whether any changed decoration carried injected-text options.
  /// Deviation: Monaco routes injected-text deltas through
  /// `onDidChangeContentOrInjectedText` with per-line granularity
  /// (`handleBeforeFireDecorationsChangedEvent` → affected-lines event);
  /// the viewer's pipeline rebuilds the projection whole, so a single
  /// event flag is the seam.
  mut affects_injected_text : Bool
  /// Carried for event shape only: `lineNumberClassName` is not ported, so
  /// nothing sets this (and, like the source's `doFire`, it is never reset).
  affects_line_number : Bool
}

///|
fn DidChangeDecorationsEmitter::DidChangeDecorationsEmitter() -> DidChangeDecorationsEmitter {
  {
    actual: Emitter(),
    deferred_cnt: 0,
    should_fire_deferred: false,
    affects_minimap: false,
    affects_overview_ruler: false,
    affects_glyph_margin: false,
    affects_injected_text: false,
    affects_line_number: false,
  }
}

///|
fn DidChangeDecorationsEmitter::begin_deferred_emit(
  self : DidChangeDecorationsEmitter,
) -> Unit {
  self.deferred_cnt += 1
}

///|
fn DidChangeDecorationsEmitter::end_deferred_emit(
  self : DidChangeDecorationsEmitter,
) -> Unit {
  self.deferred_cnt -= 1
  if self.deferred_cnt == 0 {
    if self.should_fire_deferred {
      self.do_fire()
    }
    // affected injected-text/line-height/font sets cleared here in the
    // source — N-A (features out of scope)
  }
}

///|
/// `checkAffectedAndFire`: `_affectsLineNumber ||= !!options.lineNumberClassName`
/// is dropped (field not ported). Truthiness of `overviewRuler?.color` and
/// `glyphMarginClassName` means a present, non-empty string.
fn DidChangeDecorationsEmitter::check_affected_and_fire(
  self : DidChangeDecorationsEmitter,
  options : ModelDecorationOptions,
) -> Unit {
  if !self.affects_minimap {
    self.affects_minimap = options.minimap is Some(_)
  }
  if !self.affects_overview_ruler {
    self.affects_overview_ruler = options.overview_ruler is Some(ruler) &&
      ruler.color is Some(color) &&
      color != ""
  }
  if !self.affects_glyph_margin {
    self.affects_glyph_margin = options.glyph_margin_class_name is Some(name) &&
      name != ""
  }
  if !self.affects_injected_text {
    self.affects_injected_text = options_is_injected_text(options)
  }
  self.try_fire()
}

///|
fn DidChangeDecorationsEmitter::fire(
  self : DidChangeDecorationsEmitter,
) -> Unit {
  self.affects_minimap = true
  self.affects_overview_ruler = true
  self.affects_glyph_margin = true
  self.affects_injected_text = true
  self.try_fire()
}

///|
fn DidChangeDecorationsEmitter::try_fire(
  self : DidChangeDecorationsEmitter,
) -> Unit {
  if self.deferred_cnt == 0 {
    self.do_fire()
  } else {
    self.should_fire_deferred = true
  }
}

///|
fn DidChangeDecorationsEmitter::do_fire(
  self : DidChangeDecorationsEmitter,
) -> Unit {
  // handleBeforeFire fan-out: N-A (all three arms out of scope)
  let event : ModelDecorationsChangedEvent = {
    affects_minimap: self.affects_minimap,
    affects_overview_ruler: self.affects_overview_ruler,
    affects_glyph_margin: self.affects_glyph_margin,
    affects_injected_text: self.affects_injected_text,
    affects_line_number: self.affects_line_number,
  }
  self.should_fire_deferred = false
  self.affects_minimap = false
  self.affects_overview_ruler = false
  self.affects_glyph_margin = false
  self.affects_injected_text = false
  self.actual.fire(event)
}

// #endregion

// #region ModelDecorationOptions node predicates (textModel.ts:2180-2190)

///|
/// `isNodeInOverviewRuler` (`textModel.ts:2180-2182`).
fn is_node_in_overview_ruler(node : IntervalNode) -> Bool {
  node.options is Some(options) && options_in_overview_ruler(options)
}

///|
fn options_in_overview_ruler(options : ModelDecorationOptions) -> Bool {
  options.overview_ruler is Some(ruler) &&
  ruler.color is Some(color) &&
  color != ""
}

///|
/// `isNodeInjectedText` (`textModel.ts:2188-2190`) over
/// `isOptionsInjectedText`: the node carries `before`/`after` injected-text
/// options.
fn is_node_injected_text(node : IntervalNode) -> Bool {
  node.options is Some(options) && options_is_injected_text(options)
}

///|
fn options_is_injected_text(options : ModelDecorationOptions) -> Bool {
  options.before is Some(_) || options.after is Some(_)
}

// #endregion

// #region DecorationsTrees (textModel.ts:2196-2353)

///|
/// `DecorationsTrees`: tree0 holds decorations that do not show up in the
/// overview ruler, tree1 those that do, and the injected-text tree is the
/// stubbed-empty third slot (see file header).
struct DecorationsTrees {
  decorations_tree_0 : IntervalTree
  decorations_tree_1 : IntervalTree
  injected_text_decorations_tree : IntervalTree
}

///|
fn DecorationsTrees::DecorationsTrees() -> DecorationsTrees {
  {
    decorations_tree_0: IntervalTree(),
    decorations_tree_1: IntervalTree(),
    injected_text_decorations_tree: IntervalTree(),
  }
}

///|
/// `_ensureNodesHaveRanges` (`textModel.ts:2224-2232`). Monaco fills missing
/// ranges then casts the nodes to `IModelDecoration`; the port materializes
/// `ModelDecoration` copies (same data — noted on the type).
fn DecorationsTrees::ensure_nodes_have_ranges(
  self : DecorationsTrees,
  host : TextModel,
  nodes : Array[IntervalNode],
) -> Array[ModelDecoration] {
  ignore(self)
  for node in nodes {
    if node.range is None {
      node.range = Some(
        host.get_range_at(node.cached_absolute_start, node.cached_absolute_end),
      )
    }
  }
  nodes.map(node => {
    id: node.id,
    owner_id: node.owner_id,
    range: node.range.unwrap(),
    options: node.options.unwrap(),
  })
}

///|
/// `getAllInInterval` (`textModel.ts:2233-2238`).
fn DecorationsTrees::get_all_in_interval(
  self : DecorationsTrees,
  host : TextModel,
  start : Int32,
  end : Int32,
  filter_owner_id : Int32,
  filter_out_validation : Bool,
  filter_font_decorations : Bool,
  only_margin_decorations : Bool,
) -> Array[ModelDecoration] {
  let version_id = host.get_version_id()
  let result = self.interval_search(
    start, end, filter_owner_id, filter_out_validation, filter_font_decorations,
    version_id, only_margin_decorations,
  )
  self.ensure_nodes_have_ranges(host, result)
}

///|
/// `_intervalSearch` (`textModel.ts:2240-2245`).
fn DecorationsTrees::interval_search(
  self : DecorationsTrees,
  start : Int32,
  end : Int32,
  filter_owner_id : Int32,
  filter_out_validation : Bool,
  filter_font_decorations : Bool,
  cached_version_id : Int32,
  only_margin_decorations : Bool,
) -> Array[IntervalNode] {
  let r0 = self.decorations_tree_0.interval_search(
    start, end, filter_owner_id, filter_out_validation, filter_font_decorations,
    cached_version_id, only_margin_decorations,
  )
  let r1 = self.decorations_tree_1.interval_search(
    start, end, filter_owner_id, filter_out_validation, filter_font_decorations,
    cached_version_id, only_margin_decorations,
  )
  let r2 = self.injected_text_decorations_tree.interval_search(
    start, end, filter_owner_id, filter_out_validation, filter_font_decorations,
    cached_version_id, only_margin_decorations,
  )
  r0 + r1 + r2
}

///|
/// `getAllInjectedText` (`textModel.ts`, DecorationsTrees).
fn DecorationsTrees::get_all_injected_text(
  self : DecorationsTrees,
  host : TextModel,
  filter_owner_id : Int32,
) -> Array[ModelDecoration] {
  let version_id = host.get_version_id()
  let result = self.injected_text_decorations_tree.search(
    filter_owner_id, false, false, version_id, false,
  )
  self
  .ensure_nodes_have_ranges(host, result)
  .filter(d => d.options.show_if_collapsed || !d.range.is_empty())
}

///|
/// `collectNodesFromOwner` (`textModel.ts:2328-2333`).
fn DecorationsTrees::collect_nodes_from_owner(
  self : DecorationsTrees,
  owner_id : Int32,
) -> Array[IntervalNode] {
  let r0 = self.decorations_tree_0.collect_nodes_from_owner(owner_id)
  let r1 = self.decorations_tree_1.collect_nodes_from_owner(owner_id)
  let r2 = self.injected_text_decorations_tree.collect_nodes_from_owner(
    owner_id,
  )
  r0 + r1 + r2
}

///|
/// `insert` (`textModel.ts:2342-2350` region): route by node bits.
fn DecorationsTrees::insert(
  self : DecorationsTrees,
  node : IntervalNode,
) -> Unit {
  if is_node_injected_text(node) {
    self.injected_text_decorations_tree.insert(node)
  } else if is_node_in_overview_ruler(node) {
    self.decorations_tree_1.insert(node)
  } else {
    self.decorations_tree_0.insert(node)
  }
}

///|
/// `delete`.
fn DecorationsTrees::delete(
  self : DecorationsTrees,
  node : IntervalNode,
) -> Unit {
  if is_node_injected_text(node) {
    self.injected_text_decorations_tree.delete(node)
  } else if is_node_in_overview_ruler(node) {
    self.decorations_tree_1.delete(node)
  } else {
    self.decorations_tree_0.delete(node)
  }
}

///|
/// `getNodeRange` (`textModel.ts:2327-2340` region).
fn DecorationsTrees::get_node_range(
  self : DecorationsTrees,
  host : TextModel,
  node : IntervalNode,
) -> @base_common.Range {
  let version_id = host.get_version_id()
  if node.cached_version_id != version_id {
    self.resolve_node(node, version_id)
  }
  if node.range is None {
    node.range = Some(
      host.get_range_at(node.cached_absolute_start, node.cached_absolute_end),
    )
  }
  node.range.unwrap()
}

///|
/// `_resolveNode`.
fn DecorationsTrees::resolve_node(
  self : DecorationsTrees,
  node : IntervalNode,
  cached_version_id : Int32,
) -> Unit {
  if is_node_injected_text(node) {
    self.injected_text_decorations_tree.resolve_node(node, cached_version_id)
  } else if is_node_in_overview_ruler(node) {
    self.decorations_tree_1.resolve_node(node, cached_version_id)
  } else {
    self.decorations_tree_0.resolve_node(node, cached_version_id)
  }
}

// #endregion

// #region TextModel decoration API (textModel.ts:1669-2062)

///|
/// `onDidChangeDecorations` (`textModel.ts:225-226`): one event per
/// deferred-emit bracket. The backing `_onDidChangeDecorations` emitter is
/// the same-named `TextModel` field.
pub fn TextModel::on_did_change_decorations(
  self : TextModel,
  listener : (ModelDecorationsChangedEvent) -> Unit,
) -> @base_common.Disposable {
  self.on_did_change_decorations.actual.event(listener)
}

///|
/// `getRangeAt` (`textModel.ts:1886-1889` region): offsets → line/column
/// range via the snapshot.
fn TextModel::get_range_at(
  self : TextModel,
  start : Int32,
  end : Int32,
) -> @base_common.Range {
  self.snapshot.range_of(OffsetRange(start, end))
}

///|
/// The `IModelDecorationsChangeAccessor` handed to `change_decorations`
/// callbacks (`textModel.ts:1681-1701`). Fields are invalidated after the
/// callback returns, like the source's `invalidFunc` swap.
pub struct ModelDecorationsChangeAccessor {
  mut add_decoration : (@base_common.Range, ModelDecorationOptions) -> String
  mut change_decoration : (String, @base_common.Range) -> Unit
  mut change_decoration_options : (String, ModelDecorationOptions) -> Unit
  mut remove_decoration : (String) -> Unit
  mut delta_decorations : (Array[String], Array[ModelDeltaDecoration]) -> Array[
    String,
  ]
}

///|
/// `changeDecorations` (`textModel.ts:1669-1678`): runs `callback` against a
/// change accessor inside one deferred-emit bracket.
pub fn[T] TextModel::change_decorations(
  self : TextModel,
  callback : (ModelDecorationsChangeAccessor) -> T,
  owner_id~ : Int32 = 0,
) -> T {
  // _assertNotDisposed: N-A (readonly model)
  self.on_did_change_decorations.begin_deferred_emit()
  let result = self.change_decorations_impl(owner_id, callback)
  self.on_did_change_decorations.end_deferred_emit()
  result
}

///|
/// `_changeDecorations` (`textModel.ts:1680-1715`).
fn[T] TextModel::change_decorations_impl(
  self : TextModel,
  owner_id : Int32,
  callback : (ModelDecorationsChangeAccessor) -> T,
) -> T {
  let accessor : ModelDecorationsChangeAccessor = {
    add_decoration: (range, options) => {
      self.delta_decorations_impl(owner_id, [], [{ range, options, }])[0]
    },
    change_decoration: (id, new_range) => {
      self.change_decoration_impl(owner_id, id, new_range)
    },
    change_decoration_options: (id, options) => {
      self.change_decoration_options_impl(owner_id, id, options)
    },
    remove_decoration: id => {
      ignore(self.delta_decorations_impl(owner_id, [id], []))
    },
    delta_decorations: (old_decorations, new_decorations) => {
      if old_decorations.length() == 0 && new_decorations.length() == 0 {
        // nothing to do
        []
      } else {
        self.delta_decorations_impl(owner_id, old_decorations, new_decorations)
      }
    },
  }
  let result = callback(accessor)
  // Invalidate change accessor
  accessor.add_decoration = (_range, _options) => {
    abort("Invalid change accessor")
  }
  accessor.change_decoration = (_id, _range) => abort("Invalid change accessor")
  accessor.change_decoration_options = (_id, _options) => {
    abort("Invalid change accessor")
  }
  accessor.remove_decoration = _id => abort("Invalid change accessor")
  accessor.delta_decorations = (_old, _new) => abort("Invalid change accessor")
  result
}

///|
/// `deltaDecorations` (`textModel.ts:1717-1740`): the atomic remove+add
/// primitive. Returns the new decoration ids.
pub fn TextModel::delta_decorations(
  self : TextModel,
  old_decorations : Array[String],
  new_decorations : Array[ModelDeltaDecoration],
  owner_id~ : Int32 = 0,
) -> Array[String] {
  // _assertNotDisposed: N-A (readonly model)
  if old_decorations.length() == 0 && new_decorations.length() == 0 {
    // nothing to do
    return []
  }
  self.delta_decoration_call_cnt += 1
  if self.delta_decoration_call_cnt > 1 {
    // console.warn + onUnexpectedError in the source; println is the js
    // backend's console.
    println(
      "Invoking deltaDecorations recursively could lead to leaking decorations.",
    )
  }
  self.on_did_change_decorations.begin_deferred_emit()
  let result = self.delta_decorations_impl(
    owner_id, old_decorations, new_decorations,
  )
  self.on_did_change_decorations.end_deferred_emit()
  self.delta_decoration_call_cnt -= 1
  result
}

///|
/// `removeAllDecorationsWithOwnerId` (`textModel.ts:1777-1789`). Like the
/// source, this fires no change event.
pub fn TextModel::remove_all_decorations_with_owner_id(
  self : TextModel,
  owner_id : Int32,
) -> Unit {
  // _isDisposed guard: N-A (readonly model)
  let nodes = self.decorations_tree.collect_nodes_from_owner(owner_id)
  for node in nodes {
    self.decorations_tree.delete(node)
    self.decorations.remove(node.id)
  }
}

///|
/// `getDecorationRange` (`textModel.ts:1798-1804`).
pub fn TextModel::get_decoration_range(
  self : TextModel,
  decoration_id : String,
) -> @base_common.Range? {
  match self.decorations.get(decoration_id) {
    Some(node) => Some(self.decorations_tree.get_node_range(self, node))
    None => None
  }
}

///|
/// `getLineDecorations` (`textModel.ts:1806-1812`).
pub fn TextModel::get_line_decorations(
  self : TextModel,
  line_number : Int32,
  owner_id~ : Int32 = 0,
  filter_out_validation~ : Bool = false,
  filter_font_decorations~ : Bool = false,
) -> Array[ModelDecoration] {
  if line_number < 1 || line_number > self.snapshot.line_count() {
    return []
  }
  self.get_lines_decorations(
    line_number,
    line_number,
    owner_id~,
    filter_out_validation~,
    filter_font_decorations~,
  )
}

///|
/// `getLinesDecorations` (`textModel.ts:1813-1825`). The bracket/font
/// provider merges are N-A (providers out of scope) — tree results only.
fn TextModel::get_lines_decorations(
  self : TextModel,
  start_line_number : Int32,
  end_line_number : Int32,
  owner_id~ : Int32 = 0,
  filter_out_validation~ : Bool = false,
  filter_font_decorations~ : Bool = false,
  only_margin_decorations~ : Bool = false,
) -> Array[ModelDecoration] {
  let line_count = self.snapshot.line_count()
  let start_line_number = start_line_number.clamp(min=1, max=line_count)
  let end_line_number = end_line_number.clamp(min=1, max=line_count)
  let end_column = self.snapshot.get_line_max_column(end_line_number)
  let range : @base_common.Range = Range(
    start_line_number, 1, end_line_number, end_column,
  )
  self.get_decorations_in_range_impl(
    range, owner_id, filter_out_validation, filter_font_decorations, only_margin_decorations,
  )
}

///|
/// `getDecorationsInRange` (`textModel.ts:1826-1834`). The
/// `onlyMinimapDecorations` argument is dropped — the source forwards it only
/// to the out-of-scope bracket provider. Provider merges N-A as above.
pub fn TextModel::get_decorations_in_range(
  self : TextModel,
  range : @base_common.Range,
  owner_id~ : Int32 = 0,
  filter_out_validation~ : Bool = false,
  filter_font_decorations~ : Bool = false,
  only_margin_decorations~ : Bool = false,
) -> Array[ModelDecoration] {
  let validated_range = self.validate_range(range)
  self.get_decorations_in_range_impl(
    validated_range, owner_id, filter_out_validation, filter_font_decorations, only_margin_decorations,
  )
}

///|
/// `getInjectedTextDecorations` (`textModel.ts`): every decoration carrying
/// `before`/`after` injected-text options. `owner_id = 0` matches all
/// owners (the tree's filter semantics).
pub fn TextModel::get_injected_text_decorations(
  self : TextModel,
  owner_id~ : Int32 = 0,
) -> Array[ModelDecoration] {
  self.decorations_tree.get_all_injected_text(self, owner_id)
}

///|
/// `_getDecorationsInRange` (`textModel.ts:1880-1889`).
fn TextModel::get_decorations_in_range_impl(
  self : TextModel,
  filter_range : @base_common.Range,
  filter_owner_id : Int32,
  filter_out_validation : Bool,
  filter_font_decorations : Bool,
  only_margin_decorations : Bool,
) -> Array[ModelDecoration] {
  let start_offset = self.snapshot.get_offset_at(
    filter_range.start_line_number,
    filter_range.start_column,
  )
  let end_offset = self.snapshot.get_offset_at(
    filter_range.end_line_number,
    filter_range.end_column,
  )
  self.decorations_tree.get_all_in_interval(
    self, start_offset, end_offset, filter_owner_id, filter_out_validation, filter_font_decorations,
    only_margin_decorations,
  )
}

///|
/// `_changeDecorationImpl` (`textModel.ts:1890-1935`). The injected-text/
/// line-height/font record arms are dropped with their features; `ownerId`
/// is consumed only by those arms in the source, hence unused here.
fn TextModel::change_decoration_impl(
  self : TextModel,
  _owner_id : Int32,
  decoration_id : String,
  range : @base_common.Range,
) -> Unit {
  guard self.decorations.get(decoration_id) is Some(node) else { return }
  let range = self.validate_range_relaxed(range)
  let start_offset = self.snapshot.get_offset_at(
    range.start_line_number,
    range.start_column,
  )
  let end_offset = self.snapshot.get_offset_at(
    range.end_line_number,
    range.end_column,
  )
  self.decorations_tree.delete(node)
  node.reset(self.get_version_id(), start_offset, end_offset, Some(range))
  self.decorations_tree.insert(node)
  self.on_did_change_decorations.check_affected_and_fire(node.options.unwrap())
}

///|
/// `_changeDecorationOptionsImpl` (`textModel.ts:1936-1975`). The
/// injected-text/line-height/font arms are dropped with their features;
/// `changedWhetherInjectedText` is constantly false, so only the
/// overview-ruler tree migration remains.
fn TextModel::change_decoration_options_impl(
  self : TextModel,
  _owner_id : Int32,
  decoration_id : String,
  options : ModelDecorationOptions,
) -> Unit {
  guard self.decorations.get(decoration_id) is Some(node) else { return }
  let node_was_in_overview_ruler = is_node_in_overview_ruler(node)
  let node_is_in_overview_ruler = options_in_overview_ruler(options)
  self.on_did_change_decorations.check_affected_and_fire(node.options.unwrap())
  self.on_did_change_decorations.check_affected_and_fire(options)
  let moved_in_overview_ruler = node_was_in_overview_ruler !=
    node_is_in_overview_ruler
  if moved_in_overview_ruler {
    self.decorations_tree.delete(node)
    node.set_options(options)
    self.decorations_tree.insert(node)
  } else {
    node.set_options(options)
  }
}

///|
/// `_deltaDecorationsImpl` (`textModel.ts:1976-2062`): the two-pointer
/// old/new walk with node reuse. Injected-text/line-height/font record arms
/// dropped with their features.
fn TextModel::delta_decorations_impl(
  self : TextModel,
  owner_id : Int32,
  old_decorations_ids : Array[String],
  new_decorations : Array[ModelDeltaDecoration],
  suppress_events~ : Bool = false,
) -> Array[String] {
  let version_id = self.get_version_id()
  let old_decorations_len = old_decorations_ids.length()
  let mut old_decoration_index = 0
  let new_decorations_len = new_decorations.length()
  let mut new_decoration_index = 0
  self.on_did_change_decorations.begin_deferred_emit()
  let result : Array[String] = Array::make(new_decorations_len, "")
  while old_decoration_index < old_decorations_len ||
        new_decoration_index < new_decorations_len {
    let mut node : IntervalNode? = None
    if old_decoration_index < old_decorations_len {
      // (1) get ourselves an old node
      for ;; {
        let decoration_id = old_decorations_ids[old_decoration_index]
        old_decoration_index += 1
        node = self.decorations.get(decoration_id)
        if node is Some(_) || old_decoration_index >= old_decorations_len {
          break
        }
      }
      // (2) remove the node from the tree (if it exists)
      if node is Some(found) {
        self.decorations_tree.delete(found)
        if !suppress_events {
          self.on_did_change_decorations.check_affected_and_fire(
            found.options.unwrap(),
          )
        }
      }
    }
    if new_decoration_index < new_decorations_len {
      // (3) create a new node if necessary
      let live_node = match node {
        Some(found) => found
        None => {
          self.last_decoration_id += 1
          let decoration_id = "\{self.decoration_id_namespace};\{self.last_decoration_id}"
          let created = IntervalNode(decoration_id, 0, 0)
          self.decorations[decoration_id] = created
          created
        }
      }
      // (4) initialize node
      let new_decoration = new_decorations[new_decoration_index]
      let range = self.validate_range_relaxed(new_decoration.range)
      // _normalizeOptions: the single ModelDecorationOptions constructor already
      // normalized (Deviation D5)
      let options = new_decoration.options
      let start_offset = self.snapshot.get_offset_at(
        range.start_line_number,
        range.start_column,
      )
      let end_offset = self.snapshot.get_offset_at(
        range.end_line_number,
        range.end_column,
      )
      live_node.owner_id = owner_id
      live_node.reset(version_id, start_offset, end_offset, Some(range))
      live_node.set_options(options)
      if !suppress_events {
        self.on_did_change_decorations.check_affected_and_fire(options)
      }
      self.decorations_tree.insert(live_node)
      result[new_decoration_index] = live_node.id
      new_decoration_index += 1
    } else if node is Some(found) {
      self.decorations.remove(found.id)
    }
  }
  self.on_did_change_decorations.end_deferred_emit()
  result
}

// #endregion