// 1:1 port of the model content-change event types from
// vscode/src/vs/editor/common/textModelEvents.ts (checked-in vscode submodule), plus `DidChangeContentEmitter`
// (`textModel.ts:2698-2743`) and the registered-view-model seam
// (`textModel.ts:303,449-455,1652-1667`). Only the `Flush` raw change is
// constructed today (`TextModel::set_value`); the full raw-change union is
// kept so the tier-2 incremental lane (plan
// `monaco-set-value-api-port.md`, scope cut 5) slots in without reshaping.
//
// Deviations:
// - `ModelRawContentChangedEvent.resultingSelection` (`textModelEvents.ts:473`)
// is not ported: `Selection` lives above this package (`viewer/common/core`,
// MoonBit no-cycle rule), and the readonly flush lane always carries `null`
// (only `pushEditOperations` sets it). `DidChangeContentEmitter
// .endDeferredEmit`'s `resultingSelection` write drops with it.
// - `LineInjectedText` and `ModelInjectedTextChangedEvent`
// (`textModelEvents.ts:241-302,506-513`) are not ported: the viewer's
// injected-text lane is the whole-document reproject through the
// decorations listener (`attach_model.mbt`), not per-line events.
// - `IModelContentChangedEvent.detailedReasons`/`detailedReasonsChangeLengths`
// (`textModelEvents.ts:78-84`) are not ported (`TextModelEditSource` is an
// editing-telemetry type with no readonly consumer).
// - `ModelLineHeightChanged*`/`ModelFontChanged*` events: N-A (line-height/
// font decoration features not ported, see `text_model_decorations.mbt`).
///|
/// `DidChangeContentEmitter::{begin,end}_deferred_emit` are the tier-2
/// deferred-emit seam (`textModel.ts:2716-2730`; Monaco brackets them around
/// `pushEditOperations`), consumed only by `set_value_wbtest.mbt` today;
/// anchor them so the non-test build keeps them (the `moon explain 1`
/// idiom, same as `text_model_decorations.mbt`).
let _test_only_content_emitter_anchor : Unit = ignore(
(
DidChangeContentEmitter::begin_deferred_emit,
DidChangeContentEmitter::end_deferred_emit,
),
)
///|
/// `RawContentChangedType` (`textModelEvents.ts:221-227`). The source's
/// numeric const-enum values (1-5) are not observable through this port;
/// identity is by constructor.
pub(all) enum RawContentChangedType {
Flush
LineChanged
LinesDeleted
LinesInserted
EOLChanged
} derive(Eq, Debug)
///|
/// The `ModelRawChange` union (`textModelEvents.ts:451`): `ModelRawFlush`
/// (`:233`), `ModelRawLineChanged` (`:308`), `ModelRawLinesDeleted` (`:380`),
/// `ModelRawLinesInserted` (`:406`), `ModelRawEOLChanged` (`:444`), with each
/// class's fields as labeled payloads.
#warnings("-unused_constructor")
pub enum ModelRawChange {
ModelRawFlush
ModelRawLineChanged(line_number~ : Int32, line_number_post_edit~ : Int32)
ModelRawLinesDeleted(
from_line_number~ : Int32,
to_line_number~ : Int32,
last_untouched_line_post_edit~ : Int32
)
ModelRawLinesInserted(
from_line_number~ : Int32,
from_line_number_post_edit~ : Int32,
count~ : Int32
)
ModelRawEOLChanged
} derive(Debug)
///|
/// Each class's `changeType` member (`textModelEvents.ts:234,309,381,407,445`).
fn ModelRawChange::change_type(self : ModelRawChange) -> RawContentChangedType {
match self {
ModelRawFlush => Flush
ModelRawLineChanged(..) => LineChanged
ModelRawLinesDeleted(..) => LinesDeleted
ModelRawLinesInserted(..) => LinesInserted
ModelRawEOLChanged => EOLChanged
}
}
///|
/// `ModelRawContentChangedEvent` (`textModelEvents.ts:457-500`) minus
/// `resultingSelection` (file-header deviation).
pub struct ModelRawContentChangedEvent {
changes : Array[ModelRawChange]
/// The new version id the model has transitioned to.
version_id : Int32
is_undoing : Bool
is_redoing : Bool
} derive(Debug)
///|
/// `containsEvent` (`textModelEvents.ts:483-491`).
pub fn ModelRawContentChangedEvent::contains_event(
self : ModelRawContentChangedEvent,
change_type : RawContentChangedType,
) -> Bool {
self.changes.any(change => change.change_type() == change_type)
}
///|
/// `ModelRawContentChangedEvent.merge` (`textModelEvents.ts:493-499`).
fn ModelRawContentChangedEvent::merge(
a : ModelRawContentChangedEvent,
b : ModelRawContentChangedEvent,
) -> ModelRawContentChangedEvent {
{
changes: a.changes + b.changes,
version_id: b.version_id,
is_undoing: a.is_undoing || b.is_undoing,
is_redoing: a.is_redoing || b.is_redoing,
}
}
///|
/// The public `IModelContentChangedEvent` (`textModelEvents.ts:42-85`) minus
/// `detailedReasons`/`detailedReasonsChangeLengths` (file-header deviation).
/// The I-prefix drops per the repo naming policy.
pub struct ModelContentChangedEvent {
/// Ordered from the end of the document to the beginning.
changes : Array[ModelContentChange]
/// The (new) end-of-line character.
eol : String
/// The new version id the model has transitioned to.
version_id : Int32
is_undoing : Bool
is_redoing : Bool
/// All decorations were lost with this edit; the model was reset.
is_flush : Bool
is_eol_change : Bool
} derive(Debug)
///|
/// `InternalModelContentChangeEvent` (`textModelEvents.ts:562-594`).
pub struct InternalModelContentChangeEvent {
raw_content_changed_event : ModelRawContentChangedEvent
content_changed_event : ModelContentChangedEvent
} derive(Debug)
///|
/// `InternalModelContentChangeEvent.merge` (`textModelEvents.ts:568-572`).
fn InternalModelContentChangeEvent::merge(
self : InternalModelContentChangeEvent,
other : InternalModelContentChangeEvent,
) -> InternalModelContentChangeEvent {
{
raw_content_changed_event: ModelRawContentChangedEvent::merge(
self.raw_content_changed_event,
other.raw_content_changed_event,
),
content_changed_event: merge_change_events(
self.content_changed_event,
other.content_changed_event,
),
}
}
///|
/// `_mergeChangeEvents` (`textModelEvents.ts:574-593`), minus the
/// `detailedReasons` concatenation (fields not ported).
fn merge_change_events(
a : ModelContentChangedEvent,
b : ModelContentChangedEvent,
) -> ModelContentChangedEvent {
{
changes: a.changes + b.changes,
eol: b.eol,
version_id: b.version_id,
is_undoing: a.is_undoing || b.is_undoing,
is_redoing: a.is_redoing || b.is_redoing,
is_flush: a.is_flush || b.is_flush,
// both must be true to not confuse listeners who skip such edits
is_eol_change: a.is_eol_change && b.is_eol_change,
}
}
// #region DidChangeContentEmitter (textModel.ts:2698-2743)
///|
/// `DidChangeContentEmitter` (`textModel.ts:2698`): defers and merges
/// content-change events inside a deferred-emit bracket. `hasListeners`
/// (`:2712`) is N-A (no consumer in the ported slices); `endDeferredEmit`'s
/// `resultingSelection` write drops with the unported field (file header).
struct DidChangeContentEmitter {
emitter : @base_common.Emitter[InternalModelContentChangeEvent]
mut deferred_cnt : Int32
mut deferred_event : InternalModelContentChangeEvent?
}
///|
fn DidChangeContentEmitter::DidChangeContentEmitter() -> DidChangeContentEmitter {
{ emitter: Emitter(), deferred_cnt: 0, deferred_event: None, }
}
///|
fn DidChangeContentEmitter::event(
self : DidChangeContentEmitter,
listener : (InternalModelContentChangeEvent) -> Unit,
) -> @base_common.Disposable {
self.emitter.event(listener)
}
///|
fn DidChangeContentEmitter::begin_deferred_emit(
self : DidChangeContentEmitter,
) -> Unit {
self.deferred_cnt += 1
}
///|
/// `endDeferredEmit` (`textModel.ts:2720-2730`).
fn DidChangeContentEmitter::end_deferred_emit(
self : DidChangeContentEmitter,
) -> Unit {
self.deferred_cnt -= 1
if self.deferred_cnt == 0 {
if self.deferred_event is Some(e) {
self.deferred_event = None
self.emitter.fire(e)
}
}
}
///|
/// `fire` (`textModel.ts:2732-2742`): merge into the deferred slot while a
/// bracket is open, emit immediately otherwise.
fn DidChangeContentEmitter::fire(
self : DidChangeContentEmitter,
e : InternalModelContentChangeEvent,
) -> Unit {
if self.deferred_cnt > 0 {
match self.deferred_event {
Some(deferred) => self.deferred_event = Some(deferred.merge(e))
None => self.deferred_event = Some(e)
}
return
}
self.emitter.fire(e)
}
// #endregion
// #region Registered view models (textModel.ts:303,449-455,1652-1667)
///|
/// The two callbacks the text model invokes for one registered view model.
/// Keeping this record private preserves the package direction
/// `view_model -> model` without turning Monaco's erased `IViewModel` type
/// edge into a MoonBit trait object. `active` is shared with the returned
/// registration handle so disposal can remove exactly this entry without
/// object-identity comparison.
priv struct ViewModelCallbacks {
active : Ref[Bool]
on_did_change_content_or_injected_text : (InternalModelContentChangeEvent) -> Unit
emit_content_change_event : (InternalModelContentChangeEvent) -> Unit
}
///|
/// `registerViewModel` (`textModel.ts:449-451`) as a callback registration.
/// The returned idempotent handle owns the matching `unregisterViewModel`
/// lifetime (`:453-455`); callers never compare or retain trait-object boxes.
pub fn TextModel::register_view_model(
self : TextModel,
on_did_change_content_or_injected_text : (InternalModelContentChangeEvent) -> Unit,
emit_content_change_event : (InternalModelContentChangeEvent) -> Unit,
) -> @base_common.Disposable {
let active = Ref(true)
self.view_models.push({
active,
on_did_change_content_or_injected_text,
emit_content_change_event,
})
@base_common.Disposable::from(() => {
active.val = false
self.view_models.retain(callbacks => callbacks.active.val)
})
}
///|
/// `_onDidChangeContentOrInjectedText` (`textModel.ts:1652-1667`). The
/// two-loop order is load-bearing: every view model applies its structural
/// update before any emits outgoing events or updates its cursor, so no
/// listener observes a torn state. The source's per-call
/// `try/catch onUnexpectedError` is unreachable (MoonBit callbacks here are
/// non-raising — same deviation as `change_decorations`).
fn TextModel::on_did_change_content_or_injected_text(
self : TextModel,
e : InternalModelContentChangeEvent,
) -> Unit {
for callbacks in self.view_models {
(callbacks.on_did_change_content_or_injected_text)(e)
}
for callbacks in self.view_models {
(callbacks.emit_content_change_event)(e)
}
}
// #endregion
///|
pub extend InternalModelContentChangeEvent with Debug::{to_repr}
///|
pub extend ModelContentChangedEvent with Debug::{to_repr}
///|
pub extend ModelRawChange with Debug::{to_repr}
///|
pub extend ModelRawContentChangedEvent with Debug::{to_repr}
///|
pub extend RawContentChangedType with Debug::{to_repr}
///|
pub extend RawContentChangedType with Eq::{equal, not_equal}