// The hidden-areas half of Monaco's `ViewModel`
// (`vs/editor/common/viewModel/viewModelImpl.ts`): `setHiddenAreas` with its
// per-source `HiddenAreasModel` merge and the `StableViewport`
// capture/recover pair that keeps the top of the viewport pinned while lines
// above it fold away. The folding contrib is the producer; the projected
// line collection (`view_model_lines_projected.mbt`) stores the result.

///|
/// Merges hidden-area sets contributed by independent sources — Monaco's
/// `HiddenAreasModel` (`viewModelImpl.ts:1383`). Each source (the folding
/// controller, a diff feature, ...) owns its own range list; the view model
/// consumes the union. Monaco keys sources by object identity (`unknown`);
/// the viewer keys by a caller-chosen string.
priv struct HiddenAreasModel {
  hidden_areas : Map[String, Array[@base_common.Range]]
  mut should_recompute : Bool
  mut ranges : Array[@base_common.Range]
}

///|
fn HiddenAreasModel::HiddenAreasModel() -> HiddenAreasModel {
  { hidden_areas: Map([]), should_recompute: false, ranges: [], }
}

///|
/// Replaces one source's ranges — `HiddenAreasModel.setHiddenAreas`.
fn HiddenAreasModel::set_hidden_areas(
  self : HiddenAreasModel,
  source : String,
  ranges : Array[@base_common.Range],
) -> Unit {
  if self.hidden_areas.get(source) is Some(existing) && existing == ranges {
    return
  }
  self.hidden_areas[source] = ranges
  self.should_recompute = true
}

///|
/// Tests raw per-source membership while omitting one owner. Unlike
/// `get_merged_ranges`, this read neither consumes `should_recompute` nor
/// rebuilds/replaces the cached merged union.
fn HiddenAreasModel::position_is_hidden_except_source(
  self : HiddenAreasModel,
  position : @base_common.Position,
  ignored_source : String,
) -> Bool {
  for source, ranges in self.hidden_areas {
    if source == ignored_source {
      continue
    }
    for range in ranges {
      if range.start_line_number <= position.line_number &&
        position.line_number <= range.end_line_number {
        return true
      }
    }
  }
  false
}

///|
/// The union of every source's ranges, merged into disjoint line ranges —
/// `HiddenAreasModel.getMergedRanges`. Returns the previous array when the
/// merge result is unchanged so the caller's equality check short-circuits.
fn HiddenAreasModel::get_merged_ranges(
  self : HiddenAreasModel,
) -> Array[@base_common.Range] {
  if !self.should_recompute {
    return self.ranges
  }
  self.should_recompute = false
  let new_ranges = for
    _, hidden_areas in self.hidden_areas
    new_ranges = ([] : Array[@base_common.Range]) {
    continue merge_line_range_array(new_ranges, hidden_areas)
  } nobreak {
    new_ranges
  }
  if self.ranges == new_ranges {
    return self.ranges
  }
  self.ranges = new_ranges
  self.ranges
}

///|
/// Merges two sorted disjoint line-range arrays, unioning ranges that touch
/// or overlap — `mergeLineRangeArray` (`viewModelImpl.ts:1414`).
fn merge_line_range_array(
  arr1 : Array[@base_common.Range],
  arr2 : Array[@base_common.Range],
) -> Array[@base_common.Range] {
  let result : Array[@base_common.Range] = []
  let mut i = 0
  let mut j = 0
  while i < arr1.length() && j < arr2.length() {
    let item1 = arr1[i]
    let item2 = arr2[j]
    if item1.end_line_number < item2.start_line_number - 1 {
      result.push(item1)
      i += 1
    } else if item2.end_line_number < item1.start_line_number - 1 {
      result.push(item2)
      j += 1
    } else {
      let start_line_number = item1.start_line_number.min(
        item2.start_line_number,
      )
      let end_line_number = item1.end_line_number.max(item2.end_line_number)
      result.push(Range(start_line_number, 1, end_line_number, 1))
      i += 1
      j += 1
    }
  }
  while i < arr1.length() {
    result.push(arr1[i])
    i += 1
  }
  while j < arr2.length() {
    result.push(arr2[j])
    j += 1
  }
  result
}

///|
/// Whether a validated ViewZone anchor is hidden by any source other than the
/// named owner. This is intentionally narrower than the merged coordinate
/// converter: it reads the raw source lists without mutating their cached
/// union or the projected line collection.
pub fn ViewModel::model_position_is_hidden_except_source(
  self : ViewModel,
  position : @base_common.Position,
  ignored_source : String,
) -> Bool {
  self.hidden_areas_model.position_is_hidden_except_source(
    position, ignored_source,
  )
}

///|
/// The viewport anchor captured before a hidden-area change — Monaco's
/// `StableViewport` (`viewModelImpl.ts:1458`): the model position of the
/// first view line on screen plus the scroll offset into it.
priv struct StableViewport {
  viewport_start_model_position : @base_common.Position?
  start_line_delta : Double
}

///|
/// Re-scrolls so the captured model line is back at the top of the viewport
/// — `StableViewport.recoverViewportStart`.
fn StableViewport::recover_viewport_start(
  self : StableViewport,
  lines : ViewModelLinesFromProjectedModel,
  view_layout : @view_layout.ViewLayout,
) -> Unit {
  guard self.viewport_start_model_position is Some(position) else { return }
  let view_position = lines.convert_model_position_to_view_position(
    position.line_number,
    position.column,
  )
  let view_position_top = view_layout.get_vertical_offset_for_line_number(
    view_position.line_number,
  )
  view_layout.scroll_to(scroll_top=view_position_top + self.start_line_delta)
  |> ignore
}

///|
/// Captures the viewport anchor — `ViewModel._captureStableViewport`
/// (`viewModelImpl.ts:263`), "but only if the scroll position is not at the
/// top of the file". Monaco reads its continuously tracked `ViewportStart`;
/// the viewer derives the same anchor (view line at the current scroll top,
/// pixel delta into it) from the layout on demand.
fn ViewModel::capture_stable_viewport(self : ViewModel) -> StableViewport {
  let scroll_top = self.view_layout.get_current_scroll_top()
  if scroll_top > 0 && self.line_count() > 0 {
    let view_line_number = self.view_layout.line_at_vertical_offset(scroll_top)
    let previous_viewport_start_view_position : @base_common.Position = Position(
      view_line_number,
      self.get_line_min_column(view_line_number),
    )
    let previous_viewport_start_model_position = self.lines.convert_view_position_to_model_position(
      previous_viewport_start_view_position.line_number,
      previous_viewport_start_view_position.column,
    )
    let start_line_delta = scroll_top -
      self.view_layout.get_vertical_offset_for_line_number(view_line_number)
    return {
      viewport_start_model_position: Some(
        previous_viewport_start_model_position,
      ),
      start_line_delta,
    }
  }
  { viewport_start_model_position: None, start_line_delta: 0, }
}

///|
/// Applies one source's hidden-area set — `ViewModel.setHiddenAreas`
/// (`viewModelImpl.ts:592`). Merges all sources, short-circuits when the
/// union is unchanged (unless `force_update`), pushes the result into the
/// line collection, and re-points everything hanging off the view-line axis:
/// the cursor (`_cursor.onLineMappingChanged`), the decoration caches
/// (`_decorations.onLineMappingChanged`), and the layout's line count
/// (`viewLayout.onFlushed` + `onHeightMaybeChanged`). The viewport stays
/// pinned to its first visible model line unless that line itself was
/// hidden. Returns whether the view-line mapping changed — Monaco's
/// `ViewFlushedEvent`/`ViewLineMappingChangedEvent`/`HiddenAreasChangedEvent`
/// emissions, which the frame-based caller turns into a re-render.
///
/// ```mermaid
/// flowchart LR
///   A[Replace one
source range set] --> B[Merge every source] /// B --> C{Union changed
or forced?} /// C -->|no| D[Return false] /// C -->|yes| E[Capture top anchor,
apply projected ranges] /// E --> F{Line mapping
changed?} /// F -->|yes| G[Re-point cursor,
decorations, layout] /// F -->|no| H[Keep mapping] /// G --> I[Restore visible
viewport anchor] /// H --> I /// I --> J[Emit changed-only
outgoing event] /// ``` pub fn ViewModel::set_hidden_areas( self : ViewModel, ranges : Array[@base_common.Range], source~ : String = "", force_update~ : Bool = false, with_event_batch? : (() -> Bool) -> Bool, on_line_mapping_changed? : (() -> Unit) -> Unit, ) -> Bool { self.hidden_areas_model.set_hidden_areas(source, ranges) let merged_ranges = self.hidden_areas_model.get_merged_ranges() if merged_ranges == self.previous_hidden_areas && !force_update { return false } self.previous_hidden_areas = merged_ranges.copy() let stable_viewport = self.capture_stable_viewport() let apply_hidden_areas = () => { let line_mapping_changed = self.lines.set_hidden_areas(merged_ranges) if line_mapping_changed { // Browser ViewEvents live above this common package. Hand the owner one // exact source phase around cursor reprojection: Flushed/Mapping/ // Decorations are appended before `reproject_cursor`, and the cursor // event immediately after it. Headless callers take the default direct // continuation. Layout flush and stable-viewport recovery intentionally // remain below the phase so their synchronous Scroll event is appended // later in the same outer collector. let reproject_cursor = () => { self.cursor.set_context(self.coordinates_converter.cursor_context()) } match on_line_mapping_changed { Some(callback) => callback(reproject_cursor) None => reproject_cursor() } self.decorations.on_line_mapping_changed() self.view_layout.on_flushed(self.line_count()) self.view_layout.on_height_maybe_changed() |> ignore } let first_model_line_is_hidden = match stable_viewport.viewport_start_model_position { Some(position) => merged_ranges.any(range => { range.start_line_number <= position.line_number && position.line_number <= range.end_line_number }) None => false } if !first_model_line_is_hidden { stable_viewport.recover_viewport_start(self.lines, self.view_layout) } line_mapping_changed } let line_mapping_changed = match with_event_batch { Some(run) => run(apply_hidden_areas) None => apply_hidden_areas() } // Monaco publishes the outgoing fact only after the generic ViewEvent batch // has ended. For the root Viewer, returning from `run` also completes its // deferred collector flush, including layout and stable-viewport Scroll. if line_mapping_changed { self.cursor_event_dispatcher.emit_hidden_areas_changed( HiddenAreasChangedOutgoingEvent(), ) } line_mapping_changed }