Repository: hanjinliu/tabulous (language: python)

Issue:
In-cell evaluation is added to wrong place during sorting
Following causes a bug.

```python
table = viewer.add_spreadsheet(np.zeros((5, 5))
table.proxy.set([1, 3, 2, 4, 0])
table.cell[1, 0] = "&=np.sum(df.iloc[:, 0])"
```


Discussion / hints:

Fix patch:
diff --git a/tabulous/_magicgui.py b/tabulous/_magicgui.py
index 0a450955..7be26deb 100644
--- a/tabulous/_magicgui.py
+++ b/tabulous/_magicgui.py
@@ -38,7 +38,6 @@
     import pandas as pd
     from magicgui.widgets import FunctionGui
     from matplotlib.axes import Axes
-    from .widgets import TableBase
 else:
     Axes = Any
 
@@ -307,8 +306,9 @@ class ColumnNameChoice(Container):
     A container widget with a DataFrame selection and multiple column name selections.
 
     This widget is composed of two or more ComboBox widgets. The top one is to choose a
-    DataFrame and the rest are to choose column names from the DataFrame. When the DataFrame
-    selection changed, the column name selections will also changed accordingly.
+    DataFrame and the rest are to choose column names from the DataFrame. When the
+    DataFrame selection changed, the column name selections will also changed
+    accordingly.
     """
 
     def __init__(
@@ -540,11 +540,15 @@ def _read_selection(self, table: TableBase | None = None):
             raise ValueError("More than one selection is given.")
         sel = sels[0]
 
-        qwidget = table._qwidget
+        qwidget = table.native
         column_selected = qwidget._qtable_view._selection_model._col_selection_indices
+        if isinstance(table, SpreadSheet):
+            df = table.native._data_raw
+        else:
+            df = table.data_shown
         _selop = construct(
             *sel,
-            qwidget.model().df,
+            df,
             method=self.format,
             column_selected=column_selected,
             allow_out_of_bounds=self._allow_out_of_bounds,
diff --git a/tabulous/_map_model.py b/tabulous/_map_model.py
index 614ccac3..acbcea6d 100644
--- a/tabulous/_map_model.py
+++ b/tabulous/_map_model.py
@@ -17,7 +17,6 @@
 from tabulous._psygnal import InCellRangedSlot
 
 if TYPE_CHECKING:
-    from tabulous._range import RectRange
     from tabulous.widgets import TableBase
 
 
@@ -131,24 +130,24 @@ def table(self) -> TableBase:
             return table
         raise RuntimeError("Table has been deleted")
 
-    def __getitem__(self, key: Index) -> InCellRangedSlot:
+    def __getitem__(self, source_key: Index) -> InCellRangedSlot:
         for slot in self.table().events.data.iter_slots():
             if not isinstance(slot, InCellRangedSlot):
                 continue
-            if slot.pos == key:
+            if slot.source_pos == source_key:
                 return slot
-        raise KeyError(key)
+        raise KeyError(source_key)
 
-    def get_by_dest(self, key: Index, default=None) -> InCellRangedSlot:
+    def get_by_dest(self, source_key: Index, default=None) -> InCellRangedSlot:
         for slot in self.table().events.data.iter_slots():
             if not isinstance(slot, InCellRangedSlot):
                 continue
-            if slot.pos == key:
+            if slot.source_pos == source_key:
                 return slot
             dest = slot.last_destination
             if dest is not None:
                 rsl, csl = dest
-                r, c = key
+                r, c = source_key
                 if rsl.start <= r < rsl.stop and csl.start <= c < csl.stop:
                     return slot
         return default
@@ -161,12 +160,12 @@ def __setitem__(self, key: Index, slot: InCellRangedSlot) -> None:
         self.table().events.data.connect_cell_slot(slot)
         logger.debug(f"Connecting slot at {key}")
 
-    def __delitem__(self, key: Index) -> None:
-        if self._locked_pos == key:
+    def __delitem__(self, source_key: Index) -> None:
+        if self._locked_pos == source_key:
             return
-        slot = self[key]
+        slot = self[source_key]
         self.table().events.data.disconnect(slot)
-        logger.debug(f"Deleting slot at {key}")
+        logger.debug(f"Deleting slot at {source_key}")
 
     def _remove_multiple(self, slots: Iterable[InCellRangedSlot]):
         data = self.table().events.data
@@ -177,7 +176,7 @@ def __iter__(self) -> Iterator[Index]:
         for slot in self.table().events.data.iter_slots():
             if not isinstance(slot, InCellRangedSlot):
                 continue
-            yield Index(*slot.pos)
+            yield Index(*slot.source_pos)
 
     def values(self):
         for slot in self.table().events.data.iter_slots():
@@ -189,7 +188,7 @@ def items(self):
         for slot in self.table().events.data.iter_slots():
             if not isinstance(slot, InCellRangedSlot):
                 continue
-            yield Index(*slot.pos), slot
+            yield Index(*slot.source_pos), slot
 
     def __len__(self) -> int:
         return len([i for i in self])
diff --git a/tabulous/_psygnal.py b/tabulous/_psygnal.py
index 6b1d9a45..64552b0d 100644
--- a/tabulous/_psygnal.py
+++ b/tabulous/_psygnal.py
@@ -14,19 +14,25 @@
     TypeVar,
     get_type_hints,
     Union,
+    Type,
+    NoReturn,
+    cast,
 )
-from typing_extensions import get_args, get_origin, ParamSpec, Self, Literal
+from typing_extensions import get_args, get_origin, ParamSpec, Self
+import warnings
 import weakref
 from contextlib import suppress, contextmanager
 from functools import wraps, partial, lru_cache, reduce
 import inspect
 from inspect import Parameter, Signature, isclass
-
+import threading
 import numpy as np
+
 from psygnal import EmitLoopError
 
 from tabulous._range import RectRange, AnyRange, MultiRectRange, TableAnchorBase
 from tabulous._selection_op import iter_extract_with_range
+from tabulous.exceptions import UnreachableError
 
 __all__ = ["SignalArray"]
 
@@ -35,8 +41,7 @@
 _R = TypeVar("_R")
 
 if TYPE_CHECKING:
-    from tabulous.widgets import TableBase
-    import numpy as np
+    from tabulous.widgets._table import _DataFrameTableLayer
     import pandas as pd
 
     MethodRef = tuple[weakref.ReferenceType[object], str, Union[Callable, None]]
@@ -112,9 +117,13 @@ def __init__(self, objs: list):
         self._objs = objs
 
     def eval(self, ns: dict[str, Any], ranges: MultiRectRange):
+        return self.eval_and_format(ns, ranges)[0]
+
+    def eval_and_format(self, ns: dict[str, Any], ranges: MultiRectRange):
         expr = self.as_literal(ranges)
         logger.debug(f"About to run: {expr!r}")
-        return eval(expr, ns, {})
+        out = eval(expr, ns, {})
+        return out, expr
 
     def as_literal(self, ranges: MultiRectRange) -> str:
         out: list[str] = []
@@ -135,15 +144,15 @@ def __init__(
         self,
         expr: InCellExpr,
         pos: tuple[int, int],
-        table: TableBase,
+        table: _DataFrameTableLayer,
         range: RectRange = AnyRange(),
     ):
         self._expr = expr
         super().__init__(lambda: self.call(), range)
-        self._pos = pos
         self._table = weakref.ref(table)
         self._last_destination: tuple[slice, slice] | None = None
         self._current_error: Exception | None = None
+        self.set_pos(pos)
 
     def __repr__(self) -> str:
         expr = self.as_literal()
@@ -170,7 +179,7 @@ def format_error(self) -> str:
             return f"{exc_type}: {exc_msg}"
 
     @property
-    def table(self) -> TableBase:
+    def table(self) -> _DataFrameTableLayer:
         """Get the parent table"""
         if table := self._table():
             return table
@@ -178,12 +187,19 @@ def table(self) -> TableBase:
 
     @property
     def pos(self) -> tuple[int, int]:
-        """The position of the cell that this slot is attached to."""
+        """The visual position of the cell that this slot is attached to."""
         return self._pos
 
+    @property
+    def source_pos(self) -> tuple[int, int]:
+        """The source position of the cell that this slot is attached to."""
+        return self._source_pos
+
     def set_pos(self, pos: tuple[int, int]):
         """Set the position of the cell that this slot is attached to."""
         self._pos = pos
+        prx = self.table.proxy._get_proxy_object()
+        self._source_pos = prx.get_source_index(pos[0]), pos[1]
         return self
 
     @property
@@ -204,7 +220,7 @@ def last_destination(self, val):
     @classmethod
     def from_table(
         cls: type[Self],
-        table: TableBase,
+        table: _DataFrameTableLayer,
         expr: str,
         pos: tuple[int, int],
     ) -> Self:
@@ -212,10 +228,10 @@ def from_table(
         qtable = table.native
 
         # normalize expression to iloc-slicing.
-        df_ref = qtable.dataShown(parse=False)
+        df_ref = qtable._data_raw
         current_end = 0
         output: list[str] = []
-        ranges = []
+        ranges: list[tuple[slice, slice]] = []
         for (start, end), op in iter_extract_with_range(expr):
             output.append(expr[current_end:start])
             output.append(InCellExpr.SELECT)
@@ -237,24 +253,28 @@ def evaluate(self) -> EvalResult:
         qviewer = qtable.parentViewer()
         self._current_error = None
 
-        df = qtable.dataShown(parse=True)
+        df = qtable.getDataFrame()
         ns = dict(qviewer._namespace)
         ns.update(df=df)
         try:
-            out = self._expr.eval(ns, self.range)
+            out, _expr = self._expr.eval_and_format(ns, self.range)
             logger.debug(f"Evaluated at {self.pos!r}")
         except Exception as e:
             logger.debug(f"Evaluation failed at {self.pos!r}: {e!r}")
             self._current_error = e
-            return EvalResult(e, self.pos)
+            return EvalResult(e, self.source_pos)
 
-        _row, _col = self.pos
+        _row, _col = self.source_pos
 
         _is_named_tuple = isinstance(out, tuple) and hasattr(out, "_fields")
         _is_dict = isinstance(out, dict)
         if _is_named_tuple or _is_dict:
-            with qtable_view._selection_model.blocked(), table.events.data.blocked():
+            # fmt: off
+            with qtable_view._selection_model.blocked(), \
+                table.events.data.blocked(), \
+                table.proxy.released():
                 table.cell.set_labeled_data(_row, _col, out, sep=":")
+            # fmt: on
 
             self.last_destination = (
                 slice(_row, _row + len(out)),
@@ -283,7 +303,9 @@ def evaluate(self) -> EvalResult:
         elif isinstance(out, np.ndarray):
             _out = np.squeeze(out)
             if _out.size == 0:
-                raise CellEvaluationError("Evaluation returned 0-sized array.")
+                raise CellEvaluationError(
+                    "Evaluation returned 0-sized array.", self.source_pos
+                )
             if _out.ndim == 0:  # scalar
                 _out = qtable.convertValue(_col, _out.item())
                 _row, _col = self._infer_indices()
@@ -293,7 +315,7 @@ def evaluate(self) -> EvalResult:
                 _row = slice(_row, _row + _out.shape[0])
                 _col = slice(_col, _col + _out.shape[1])
             else:
-                raise CellEvaluationError("Cannot assign a >3D array.", self.pos)
+                raise CellEvaluationError("Cannot assign a >3D array.", self.source_pos)
 
         else:
             _out = qtable.convertValue(_col, out)
@@ -307,12 +329,15 @@ def evaluate(self) -> EvalResult:
             _out = str(_out)
 
         else:
-            raise RuntimeError(_row, _col)  # Unreachable
+            raise UnreachableError(type(_row), type(_col))
 
         _sel_model = qtable_view._selection_model
-        with _sel_model.blocked(), qtable_view._table_map.lock_pos(self.pos):
+        # fmt: off
+        with _sel_model.blocked(), \
+            qtable_view._table_map.lock_pos(self.pos), \
+            table.proxy.released(keep_widgets=True):
             qtable.setDataFrameValue(_row, _col, _out)
-
+        # fmt: on
         self.last_destination = (_row, _col)
         return EvalResult(out, (_row, _col))
 
@@ -337,10 +362,13 @@ def after_called(self, out: EvalResult) -> None:
                 err_repr,
                 dtype=object,
             )
-            with qtable_view._selection_model.blocked():
-                with qtable_view._table_map.lock_pos(self.pos):
-                    with table.events.data.blocked():
-                        table._qwidget.setDataFrameValue(rsl, csl, pd.DataFrame(val))
+            # fmt: off
+            with qtable_view._selection_model.blocked(), \
+                qtable_view._table_map.lock_pos(self.pos), \
+                table.events.data.blocked(), \
+                table.proxy.released(keep_widgets=True):
+                table._qwidget.setDataFrameValue(rsl, csl, pd.DataFrame(val))
+            # fmt: on
         return None
 
     def call(self):
@@ -429,7 +457,7 @@ def _infer_indices(self) -> tuple[int, int]:
         return r, c
 
     def _infer_slices(self, out: pd.Series | np.ndarray) -> tuple[slice, slice]:
-        """Infer how to concatenate ``out`` to ``df``."""
+        """Infer how to concatenate ``out`` to ``df``, based on the selections"""
         #  x | x | x |     1. Self-update is not safe. Raise Error.
         #  x |(1)| x |(2)  2. Return as a column vector.
         #  x | x | x |     3. Return as a row vector.
@@ -492,10 +520,6 @@ def __init__(self, msg: str, pos: tuple[int, int]) -> None:
         self._pos = pos
 
 
-import threading
-import warnings
-from typing import NoReturn, Type, cast
-
 _NULL = object()
 
 
@@ -1255,7 +1279,7 @@ def remove_rows(self, row: int, count: int):
             if isinstance(slot, RangedSlot):
                 slot.remove_rows(row, count)
                 if slot.range.is_empty():
-                    logger.debug(f"Range became empty by removing rows")
+                    logger.debug("Range became empty by removing rows")
                     to_be_disconnected.append(slot)
         for slot in to_be_disconnected:
             self.disconnect(slot, missing_ok=False)
@@ -1268,7 +1292,7 @@ def remove_columns(self, col: int, count: int):
             if isinstance(slot, RangedSlot):
                 slot.remove_columns(col, count)
                 if slot.range.is_empty():
-                    logger.debug(f"Range became empty by removing columns")
+                    logger.debug("Range became empty by removing columns")
                     to_be_disconnected.append(slot)
         for slot in to_be_disconnected:
             self.disconnect(slot, missing_ok=False)
diff --git a/tabulous/_qt/_proxy_button.py b/tabulous/_qt/_proxy_button.py
index 964d306c..1d1a18a1 100644
--- a/tabulous/_qt/_proxy_button.py
+++ b/tabulous/_qt/_proxy_button.py
@@ -16,6 +16,8 @@
 )
 from superqt import QEnumComboBox
 
+from tabulous.exceptions import UnreachableError
+
 if TYPE_CHECKING:
     from typing_extensions import Self
     from tabulous._qt._table import QBaseTable
@@ -255,7 +257,7 @@ def _type_changed(self, val: FilterType):
             self._string_edit.setVisible(True)
             self._string_edit.setFocus()
         elif val is not FilterType.none:
-            raise RuntimeError(f"Unreachable: {val}")
+            raise UnreachableError(f"Unreachable: {val}")
         self.requireResize.emit(self.sizeHint())
 
     def _call_button_clicked(self):
diff --git a/tabulous/_qt/_table/_base/_enhanced_table.py b/tabulous/_qt/_table/_base/_enhanced_table.py
index b1f06147..cccb630d 100644
--- a/tabulous/_qt/_table/_base/_enhanced_table.py
+++ b/tabulous/_qt/_table/_base/_enhanced_table.py
@@ -204,14 +204,18 @@ def _tab_clicked(self) -> None:
     def _on_moving(self, src: Index, dst: Index) -> None:
         _need_update_all = self._current_drawing_slot_ranges is not None
         viewer = self.parentViewer()._table_viewer
-        if slot := self._table_map.get_by_dest(dst, None):
-            self._current_drawing_slot_ranges = slot.range
-            viewer.status = f"<b><code>{slot.as_literal(dest=True)}</code></b>"
-            _need_update_all = True
-        else:
-            if self._current_drawing_slot_ranges is not None:
-                viewer.status = ""
-            self._current_drawing_slot_ranges = None
+        _nr, _nc = self.parentTable().dataShape()
+        _r0, _c0 = dst
+        if _r0 < _nr and _c0 < _nc:
+            _r0 = self.parentTable()._proxy.get_source_index(_r0)
+            if slot := self._table_map.get_by_dest((_r0, _c0), None):
+                self._current_drawing_slot_ranges = slot.range
+                viewer.status = f"<b><code>{slot.as_literal(dest=True)}</code></b>"
+                _need_update_all = True
+            else:
+                if self._current_drawing_slot_ranges is not None:
+                    viewer.status = ""
+                self._current_drawing_slot_ranges = None
 
         if _need_update_all:
             self._update_all()
diff --git a/tabulous/_qt/_table/_base/_item_model.py b/tabulous/_qt/_table/_base/_item_model.py
index a6bbaa56..e75fb464 100644
--- a/tabulous/_qt/_table/_base/_item_model.py
+++ b/tabulous/_qt/_table/_base/_item_model.py
@@ -122,8 +122,8 @@ def _data_text_color(self, index: QtCore.QModelIndex):
                         return QtCore.QVariant()
                     rgba = normalize_color(col)
                 except Exception as e:
-                    # since this method is called many times, errorous function should be
-                    # deleted from the mapper.
+                    # since this method is called many times, errorous function should
+                    # be deleted from the mapper.
                     self._foreground_colormap.pop(colname)
                     raise e
                 return QtGui.QColor(*rgba)
@@ -136,7 +136,7 @@ def _data_tooltip(self, index: QtCore.QModelIndex):
         if r < self.df.shape[0] and c < self.df.shape[1]:
             val = self.df.iat[r, c]
             dtype = self.df.dtypes.values[c]
-            if ref_expr := self.parent()._get_ref_expr(r, c):
+            if ref_expr := self.parent()._get_ref_expr_by_dest(r, c):
                 ref = f"\nExpr: {ref_expr}"
             else:
                 ref = ""
@@ -161,8 +161,8 @@ def _data_background_color(self, index: QtCore.QModelIndex):
                         return QtCore.QVariant()
                     rgba = normalize_color(col)
                 except Exception as e:
-                    # since this method is called many times, errorous function should be
-                    # deleted from the mapper.
+                    # since this method is called many times, errorous function should
+                    # be deleted from the mapper.
                     self._background_colormap.pop(colname)
                     raise e
                 return QtGui.QColor(*rgba)
diff --git a/tabulous/_qt/_table/_base/_line_edit.py b/tabulous/_qt/_table/_base/_line_edit.py
index dbf17835..d18dd5de 100644
--- a/tabulous/_qt/_table/_base/_line_edit.py
+++ b/tabulous/_qt/_table/_base/_line_edit.py
@@ -10,6 +10,7 @@
 
 from tabulous._qt._qt_const import MonospaceFontFamily
 from tabulous._keymap import QtKeys
+from tabulous.exceptions import UnreachableError
 from tabulous.types import HeaderInfo, EvalInfo
 from tabulous._range import RectRange, MultiRectRange
 from tabulous._utils import get_config
@@ -155,7 +156,6 @@ def attachToolTip(self, text: str) -> None:
             point = self.mapToGlobal(self.rect().bottomLeft())
             point.setX(point.x() - 2)
             point.setY(point.y() - 10)
-            QtW.QToolTip.setFont(QtGui.QFont("Arial", 10))
             QtW.QToolTip.showText(point, text, self)
 
         return None
@@ -320,9 +320,13 @@ def __init__(
         self,
         parent: QtCore.QObject | None = None,
         table: QMutableTable | None = None,
-        pos: tuple[int, int] = (0, 0),
+        pos: tuple[int, int] = (0, 0),  # the "visual" position
     ):
         super().__init__(parent, table, pos)
+        if table is not None:
+            self._source_pos = table._proxy.get_source_index(pos[0]), pos[1]
+        else:
+            self._source_pos = pos
         self._old_range: RectRange | None = None
         qtable = self.parentTableView()
         qtable._selection_model.moved.connect(self._on_selection_changed)
@@ -378,7 +382,7 @@ def _parse_ref(cls, raw_text: str) -> tuple[str, bool]:
             text = raw_text.lstrip(cls._EVAL_PREFIX).strip()
             is_ref = False
         else:
-            raise RuntimeError("Unreachable")
+            raise UnreachableError(raw_text)
         return text, is_ref
 
     @property
@@ -456,12 +460,18 @@ def close(self) -> None:
     def eval_and_close(self) -> None:
         """Evaluate the text and close this editor."""
         if self._mode is self.Mode.TEXT:
+            # setDataFrameValue itself considers proxy. Here we just need to
+            # pass "_pos".
             self._table.setDataFrameValue(*self._pos, self.text())
             self.close()
         else:
             text, is_ref = self._parse_ref(self.text())
-            row, col = self._pos
-            info = EvalInfo(row=row, column=col, expr=text, is_ref=is_ref)
+            info = EvalInfo(
+                pos=self._pos,
+                source_pos=self._source_pos,
+                expr=text,
+                is_ref=is_ref,
+            )
             self._table.evaluatedSignal.emit(info)
         return None
 
@@ -477,12 +487,12 @@ def _on_tab_clicked(self) -> None:
             nchar = len(self.text())
             self.setSelection(nchar, nchar)
         else:
-            raise RuntimeError("Unreachable")
+            raise UnreachableError(self.mode)
 
     def _is_text_valid(self) -> bool:
         """Try to parse the text and return True if it is valid."""
         if self.mode is self.Mode.TEXT:
-            r, c = self._pos
+            _, c = self._pos
             try:
                 convert_value = self._table._get_converter(c)
                 convert_value(c, self.text())
@@ -512,7 +522,7 @@ def _is_text_valid(self) -> bool:
                 self.current_exception = None
             return True
         else:
-            raise RuntimeError("Unreachable")
+            raise UnreachableError(self.mode)
 
     def _ensure_visible(self, text: str) -> None:
         row, col = self._pos
@@ -556,7 +566,11 @@ def _on_selection_changed(self) -> None:
 
         column_selected = len(qtable_view._selection_model._col_selection_indices) > 0
         selop = construct(
-            rsl, csl, _df_ori, method="iloc", column_selected=column_selected
+            rsl,
+            csl,
+            _df_ori,
+            method="iloc",
+            column_selected=column_selected,
         )
 
         if selop is None:  # out of bound
diff --git a/tabulous/_qt/_table/_base/_table_base.py b/tabulous/_qt/_table/_base/_table_base.py
index c2234c4a..90739f5b 100644
--- a/tabulous/_qt/_table/_base/_table_base.py
+++ b/tabulous/_qt/_table/_base/_table_base.py
@@ -25,7 +25,11 @@
 from tabulous._keymap import QtKeys, QtKeyMap
 from tabulous._qt._action_registry import QActionRegistry
 from tabulous.types import ProxyType, ItemInfo, HeaderInfo, EvalInfo
-from tabulous.exceptions import SelectionRangeError, TableImmutableError
+from tabulous.exceptions import (
+    SelectionRangeError,
+    TableImmutableError,
+    UnreachableError,
+)
 from tabulous._pd_index import as_not_ranged, as_constructor, is_ranged
 
 if TYPE_CHECKING:
@@ -196,8 +200,12 @@ def tableShape(self) -> tuple[int, int]:
         return (nr, nc)
 
     def dataShape(self) -> tuple[int, int]:
+        """Shape of the visible data"""
         return self.tableShape()
 
+    def dataShapeRaw(self) -> tuple[int, int]:
+        return self.getDataFrame().shape
+
     def zoom(self) -> float:
         """Get current zoom factor."""
         return self._qtable_view.zoom()
@@ -457,6 +465,10 @@ def _apply_proxy(self):
     def _set_proxy(self, proxy: ProxyType):
         return arguments(self.proxy())
 
+    @_set_proxy.set_formatter
+    def _set_proxy_fmt(self, proxy: ProxyType):
+        return f"table.proxy.set({proxy!r})"
+
     @_mgr.interface
     def setHorizontalHeaderWidget(self, idx: int, widget: QtW.QWidget | None):
         hheader = self._qtable_view.horizontalHeader()
@@ -580,12 +592,10 @@ def setInCellSlot(self, pos: tuple[int, int], slot: InCellRangedSlot):
     @_mgr.interface
     def _set_incell_slot(self, pos: tuple[int, int], slot: InCellRangedSlot):
         """Set in-cell slot at the given position undoably."""
-        r, c = pos
-        r = self._proxy.get_source_index(r)
         if slot is None:
             self._qtable_view._table_map.pop(pos, None)
         else:
-            self._qtable_view._table_map[(r, c)] = slot
+            self._qtable_view._table_map[pos] = slot
             if dest := slot.last_destination:
                 self._qtable_view._selection_model.set_ranges([dest])
         return None
@@ -596,8 +606,8 @@ def _set_incell_slot(self, pos: tuple[int, int], slot: InCellRangedSlot):
         return arguments(pos, slot)
 
     @_set_incell_slot.set_formatter
-    def _set_incell_slot_fmt(self, pos, slot):
-        return f"table.cell[{pos[0]}, {pos[1]}] = {slot}"
+    def _set_incell_slot_fmt(self, pos, slot: InCellRangedSlot):
+        return f"table.cell[{pos[0]}, {pos[1]}] = '&={slot.as_literal()}'"
 
     def refreshTable(self, process: bool = False) -> None:
         """Refresh table view."""
@@ -846,10 +856,18 @@ def _switch_head_and_index(self, axis: int = 1) -> None:
 
     def _get_ref_expr(self, r: int, c: int) -> str | None:
         """Try to get a reference expression for the cell at (r, c)."""
+        r = self._proxy.get_source_index(r)
         if slot := self._qtable_view._table_map.get((r, c), None):
             return slot.as_literal()
         return None
 
+    def _get_ref_expr_by_dest(self, r: int, c: int) -> str | None:
+        """Try to get a reference expression for the cell at (r, c)."""
+        r = self._proxy.get_source_index(r)
+        if slot := self._qtable_view._table_map.get_by_dest((r, c), None):
+            return slot.as_literal()
+        return None
+
     def _delete_selected_highlights(self) -> None:
         """Delete the selected highlight."""
         self._qtable_view._highlight_model.delete_selected()
@@ -914,7 +932,7 @@ def setDataFrameValue(self, r: _Sliceable, c: _Sliceable, value: Any) -> None:
             raise TableImmutableError("Table is immutable.")
         data = self._data_raw
 
-        with self._mgr.merging(lambda _: _set_value_fmt(r, c, value)):
+        with self._mgr.merging(lambda _: self._set_value_fmt(r, c, value)):
             # convert values
             if isinstance(r, slice) and isinstance(c, slice):
                 # delete references
@@ -968,7 +986,7 @@ def setLabeledData(self, r: slice, c: slice, value: pd.Series):
             raise ValueError("Only one column can be set at a time.")
         data = self._data_raw
 
-        with self._mgr.merging(lambda _: _set_value_fmt(r, c, value)):
+        with self._mgr.merging(lambda _: self._set_value_fmt(r, c, value)):
             # delete references
             self._clear_incell_slots(r, c)
 
@@ -992,7 +1010,7 @@ def setLabeledData(self, r: slice, c: slice, value: pd.Series):
                 elif isinstance(r0, np.ndarray):
                     _iter = r0
                 else:
-                    raise RuntimeError(f"Unreachable data type {type(r0)!r}.")
+                    raise UnreachableError(f"{type(r0)!r}.")
                 for _i, _r in enumerate(_iter):
                     self.setItemLabel(_r, c.start, value.index[_i])
         return None
@@ -1377,10 +1395,26 @@ def redo(self) -> Any:
         """Redo last undo operation."""
         return self._mgr.redo()
 
+    @staticmethod
+    def _set_value_fmt(r, c, value):
+        _r = fmt_slice(r)
+        _c = fmt_slice(c)
+        if isinstance(value, pd.DataFrame):
+            if value.size < 6:
+                _val = str(value.values.tolist())
+            else:
+                _val = "..."
+        else:
+            _val = fmt.map_object(value)
+        return f"df.iloc[{_r}, {_c}] = {_val}"
+
 
 class QMutableSimpleTable(QMutableTable):
     """A mutable table with a single QTableView."""
 
+    def dataShapeRaw(self) -> tuple[int, int]:
+        return self._data_raw.shape
+
     @property
     def _qtable_view(self) -> _QTableViewEnhanced:
         return self._qtable_view_
@@ -1497,16 +1531,3 @@ def _to_clipboard(s: str, excel: bool = True, **kwargs) -> None:
     from pandas.io.clipboards import to_clipboard
 
     return to_clipboard(s, excel=excel, **kwargs)
-
-
-def _set_value_fmt(r, c, value):
-    _r = fmt_slice(r)
-    _c = fmt_slice(c)
-    if isinstance(value, pd.DataFrame):
-        if value.size < 6:
-            _val = str(value.values.tolist())
-        else:
-            _val = "..."
-    else:
-        _val = fmt.map_object(value)
-    return f"df.iloc[{_r}, {_c}] = {_val}"
diff --git a/tabulous/_qt/_table/_spreadsheet.py b/tabulous/_qt/_table/_spreadsheet.py
index 53d735dd..7cac99bf 100644
--- a/tabulous/_qt/_table/_spreadsheet.py
+++ b/tabulous/_qt/_table/_spreadsheet.py
@@ -124,10 +124,10 @@ def _data_tooltip(self, index: QtCore.QModelIndex):
         if r < self.df.shape[0] and c < self.df.shape[1]:
             val = self.df.iat[r, c]
             name = self.df.columns[c]
-            if ref_expr := self.parent()._get_ref_expr(r, c):
-                ref = f"\nExpr: {ref_expr}"
-            if slot := self.parent()._qtable_view._table_map.get((r, c), None):
-                ref = f"\nExpr: {slot.as_literal()}"
+            qtable = self.parent()
+            r = qtable._proxy.get_source_index(r)
+            if slot := qtable._qtable_view._table_map.get_by_dest((r, c), None):
+                ref = f"\nExpr: {slot.as_literal(dest=True)}"
                 if slot._current_error is not None:
                     ref += "\n" + slot.format_error()
             else:
@@ -225,7 +225,7 @@ def _get_sub_frame(self, columns: list[str]) -> pd.DataFrame:
 
     def dataShape(self) -> tuple[int, int]:
         """Shape of data."""
-        return self._data_raw.shape
+        return self.model().df.shape
 
     def dataShown(self, parse: bool = False) -> pd.DataFrame:
         """Return the shown dataframe (consider filter)."""
@@ -367,7 +367,7 @@ def setDataFrameValue(self, r: int | slice, c: int | slice, value: Any) -> None:
         elif isinstance(value, pd.DataFrame) and any(value.dtypes != "string"):
             value = value.astype(_STRING_DTYPE)
 
-        with self._mgr.merging(formatter=lambda cmds: cmds[-2].format()):
+        with self._mgr.merging(formatter=lambda _: self._set_value_fmt(r, c, value)):
             if need_expand:
                 self.expandDataFrame(max(rmax - nr + 1, 0), max(cmax - nc + 1, 0))
             super().setDataFrameValue(r, c, value)
diff --git a/tabulous/_selection_op.py b/tabulous/_selection_op.py
index 3e836d47..ac857dc3 100644
--- a/tabulous/_selection_op.py
+++ b/tabulous/_selection_op.py
@@ -4,6 +4,8 @@
 from functools import singledispatch
 import re
 
+from tabulous.exceptions import UnreachableError
+
 if TYPE_CHECKING:
     import pandas as pd
     from typing_extensions import Self
@@ -355,7 +357,7 @@ def parse(expr: str, *, df_expr: str = "df") -> SelectionOperator:
         sel = ValueSelOp(rsl, csl)
 
     else:
-        raise ValueError(f"Unreachable expression: {expr!r}")
+        raise UnreachableError(f"{expr!r}")
 
     return sel
 
diff --git a/tabulous/_sort_filter_proxy.py b/tabulous/_sort_filter_proxy.py
index 3483ae94..7de8be3f 100644
--- a/tabulous/_sort_filter_proxy.py
+++ b/tabulous/_sort_filter_proxy.py
@@ -123,6 +123,8 @@ def get_source_index(self, r):
             if self._array_is_bool(sl):
                 sl = np.where(sl)[0]
             r0 = sl[r]
+            if isinstance(r0, np.integer):
+                r0 = int(r0)
         return r0
 
     def get_source_slice(self, r: slice, force_single_row: bool = False) -> slice:
diff --git a/tabulous/commands/_random_data.py b/tabulous/commands/_random_data.py
index 0c6e4e00..a0eb77e7 100644
--- a/tabulous/commands/_random_data.py
+++ b/tabulous/commands/_random_data.py
@@ -12,6 +12,7 @@
     Widget,
 )
 from tabulous._magicgui import SelectionWidget
+from tabulous.exceptions import UnreachableError
 
 if TYPE_CHECKING:
     import pandas as pd
@@ -140,7 +141,7 @@ def _current_widget(self) -> _RandomGenerator:
         elif self._radio_buttons.value is Generator.choices:
             return self._choices_wdt
         else:
-            raise RuntimeError("Unreachable error happened.")
+            raise UnreachableError(self._radio_buttons.value)
 
     def get_value(self, df: pd.DataFrame) -> tuple[slice, slice, np.ndarray]:
         selop = self._selection_wdt.value
diff --git a/tabulous/commands/_statistics.py b/tabulous/commands/_statistics.py
index 31fff8ca..cd182547 100644
--- a/tabulous/commands/_statistics.py
+++ b/tabulous/commands/_statistics.py
@@ -16,6 +16,7 @@
 
 from tabulous._magicgui import find_current_table, SelectionWidget
 from tabulous._selection_op import SelectionOperator
+from tabulous.exceptions import UnreachableError
 from tabulous.widgets import Table
 
 
@@ -81,7 +82,7 @@ def extract_dataset(self, df: pd.DataFrame) -> dict[str, np.ndarray]:
         label = np.asarray(label).ravel()
         data = np.asarray(data).ravel()
         unique_labels = np.unique(label)
-        return {l: data[label == l] for l in unique_labels}
+        return {_l: data[label == _l] for _l in unique_labels}
 
 
 class UnstructuredDataContainer(DataContainer):
@@ -148,7 +149,7 @@ def run_test(self, data: Sequence[np.ndarray]):
         elif self._test_type.value == "Related":
             return stats.ttest_rel(data[0], data[1], **kwargs)
         else:
-            raise RuntimeError("Unreachable")
+            raise UnreachableError(self._test_type)
 
 
 class MannwhitneyuRunner(TestRunner):
diff --git a/tabulous/exceptions.py b/tabulous/exceptions.py
index f77ab012..787b6f55 100644
--- a/tabulous/exceptions.py
+++ b/tabulous/exceptions.py
@@ -22,6 +22,10 @@ class TableNotOrderedError(ValueError):
     """Raised when an operation is undefined if table is not ordered."""
 
 
+class UnreachableError(RuntimeError):
+    """Raised when an unreachable code is reached."""
+
+
 class ExceptionHandler:
     """Handle exceptions in the GUI thread."""
 
diff --git a/tabulous/types.py b/tabulous/types.py
index 69b121d6..3d7f76f6 100644
--- a/tabulous/types.py
+++ b/tabulous/types.py
@@ -168,18 +168,18 @@ class EvalInfo(NamedTuple):
 
     Value takes (row, column, expr, old_value, is_ref) where
 
-    row : int
-        Row index where item edited.
-    column : int
-        Column index where item edited.
+    pos : tuple[int, int]
+        The visual position of the cell where expression is evaluated.
+    source_pos : tuple[int, int]
+        The source position of the cell where expression is evaluated.
     expr : str
         Expression to be evaluated.
     is_ref: bool
         Whether the expression take references in the table.
     """
 
-    row: int
-    column: int
+    pos: tuple[int, int]
+    source_pos: tuple[int, int]
     expr: str
     is_ref: bool
 
diff --git a/tabulous/widgets/_component/_cell.py b/tabulous/widgets/_component/_cell.py
index 56459876..f19fdad5 100644
--- a/tabulous/widgets/_component/_cell.py
+++ b/tabulous/widgets/_component/_cell.py
@@ -73,9 +73,11 @@ def __setitem__(self, key: tuple[int | slice, int | slice], value: Any) -> None:
         if isinstance(value, str) and QCellLiteralEdit._is_eval_like(value):
             if row.stop - row.start == 1 and col.stop - col.start == 1:
                 expr, is_ref = QCellLiteralEdit._parse_ref(value)
+                _r0, _c0 = row.start, col.start
+                _r1 = table.proxy._get_proxy_object().get_source_index(_r0)
                 info = EvalInfo(
-                    row=row.start,
-                    column=col.start,
+                    pos=(_r0, _c0),
+                    source_pos=(_r1, _c0),
                     expr=expr,
                     is_ref=is_ref,
                 )
diff --git a/tabulous/widgets/_component/_proxy.py b/tabulous/widgets/_component/_proxy.py
index f9a6c0bc..8dafb6d8 100644
--- a/tabulous/widgets/_component/_proxy.py
+++ b/tabulous/widgets/_component/_proxy.py
@@ -1,6 +1,7 @@
 from __future__ import annotations
 
 from functools import wraps
+from contextlib import contextmanager
 from typing import (
     TYPE_CHECKING,
     Sequence,
@@ -212,6 +213,23 @@ def _wrapped(df: pd.DataFrame) -> ProxyType:
             return self._set_value(_proxy)
         return self._set_value(proxy)
 
+    @contextmanager
+    def released(self, keep_widgets: bool = False):
+        """Release the proxy in this context."""
+        proxy = self.obj
+        qtable = self.parent.native
+        if keep_widgets:
+            widgets = qtable._header_widgets().copy()
+        else:
+            widgets = {}
+        try:
+            self._set_value(None)
+            yield
+        finally:
+            self._set_value(proxy)
+            if widgets:
+                qtable.updateHorizontalHeaderWidget(widgets)
+
     def apply(self, df: pd.DataFrame) -> pd.DataFrame:
         """Apply the current proxy to the DataFrame."""
         return self._get_proxy_object().apply(df)
diff --git a/tabulous/widgets/_mainwindow.py b/tabulous/widgets/_mainwindow.py
index 737190a5..eccb2de6 100644
--- a/tabulous/widgets/_mainwindow.py
+++ b/tabulous/widgets/_mainwindow.py
@@ -162,7 +162,7 @@ def __init__(
     ):
         from tabulous._qt import get_app
 
-        app = get_app()
+        app = get_app()  # noqa: F841
         self._qwidget = self._qwidget_class(tab_position=tab_position)
         self._qwidget._table_viewer = self
         self._tablist = TableList(parent=self)
@@ -575,6 +575,7 @@ def __init__(
         show: bool = True,
     ):
         self._dock_widgets = weakref.WeakValueDictionary()
+        self._status = ""
         super().__init__(
             tab_position=tab_position,
             show=show,
@@ -583,11 +584,12 @@ def __init__(
     @property
     def status(self) -> str:
         """Return the statup tip"""
-        return self._qwidget.statusTip()
+        return self._status
 
     @status.setter
     def status(self, tip: str) -> None:
         """Set the status tip"""
+        self._status = tip
         return self._qwidget.statusBar().showMessage(tip)
 
     def add_dock_widget(
diff --git a/tabulous/widgets/_table.py b/tabulous/widgets/_table.py
index 84cbb9f9..075755f0 100644
--- a/tabulous/widgets/_table.py
+++ b/tabulous/widgets/_table.py
@@ -115,7 +115,7 @@ def __init__(
         if self.mutable:
             with self._qwidget._mgr.blocked():
                 self._qwidget.setEditable(editable)
-            self._qwidget.connectItemChangedSignal(
+            self.native.connectItemChangedSignal(
                 self._emit_data_changed_signal,
                 self.events.index.emit,
                 self.events.columns.emit,
@@ -195,8 +195,8 @@ def data_shown(self) -> pd.DataFrame:
         return self._qwidget.dataShown(parse=True)
 
     @property
-    def mutable(self) -> bool:
-        """Mutability of the table."""
+    def mutable(self):
+        """Mutability of the table type."""
         from tabulous._qt._table import QMutableTable
 
         return isinstance(self._qwidget, QMutableTable)
@@ -551,11 +551,10 @@ def _emit_evaluated(self, info: EvalInfo):
         if info.expr == "":
             return None
 
-        pos = (info.row, info.column)
         qtable = self.native
         qtable_view = qtable._qtable_view
 
-        slot = InCellRangedSlot.from_table(self, info.expr, pos)
+        slot = InCellRangedSlot.from_table(self, info.expr, info.pos)
 
         def _raise(e: Exception):
             # the common exception handling
@@ -566,7 +565,7 @@ def _raise(e: Exception):
                 except RuntimeError:
                     pass
                 with qtable_view._selection_model.blocked():
-                    qtable.setDataFrameValue(*pos, "#ERROR")
+                    qtable.setDataFrameValue(*info.pos, "#ERROR")
                 return None
             # SyntaxError/AttributeError might be caused by mistouching. Don't close
             # the editor.
@@ -578,17 +577,19 @@ def _raise(e: Exception):
             if e := result.get_err():
                 _raise(e)
             else:
-                self.move_iloc(*pos)
+                self.move_iloc(*info.pos)
         else:
             if next(iter(slot.range), None) is None:
                 # if no reference exists, evaluate the expression as "=..." form.
-                return self._emit_evaluated(EvalInfo(*pos, info.expr, False))
+                return self._emit_evaluated(
+                    EvalInfo(info.pos, info.source_pos, info.expr, False)
+                )
             with qtable._mgr.merging(formatter=lambda cmds: cmds[-1].format()):
                 # call here to properly update undo stack
                 result = slot.evaluate()
                 if e := result.get_err():
                     _raise(e)
-                qtable.setInCellSlot(pos, slot)
+                qtable.setInCellSlot(info.source_pos, slot)
 
         del qtable_view._focused_widget
         return None
@@ -661,6 +662,7 @@ class _DataFrameTableLayer(TableBase):
     """Table layer for DataFrame."""
 
     _qwidget: QTableLayer | QSpreadSheet
+    native: QTableLayer | QSpreadSheet
 
     def _normalize_data(self, data) -> pd.DataFrame:
         import pandas as pd
@@ -712,6 +714,7 @@ class Table(_DataFrameTableLayer):
 
     _Default_Name = "table"
     _qwidget: QTableLayer
+    native: Table
 
     def _create_backend(self, data: pd.DataFrame) -> QTableLayer:
         from tabulous._qt import QTableLayer
@@ -732,6 +735,7 @@ class SpreadSheet(_DataFrameTableLayer):
     """
 
     _qwidget: QSpreadSheet
+    native: QSpreadSheet
     _Default_Name = "sheet"
     dtypes = ColumnDtypeInterface()
 
@@ -803,6 +807,7 @@ class GroupBy(TableBase):
 
     _Default_Name = "groupby"
     _qwidget: QTableGroupBy
+    native: QTableGroupBy
 
     def _create_backend(self, data: pd.DataFrame) -> QTableGroupBy:
         from tabulous._qt import QTableGroupBy
@@ -856,6 +861,7 @@ class TableDisplay(TableBase):
 
     _Default_Name = "display"
     _qwidget: QTableDisplay
+    native: QTableDisplay
 
     def _create_backend(self, data: Callable[[], Any]) -> QTableDisplay:
         from tabulous._qt import QTableDisplay


Test patch:
diff --git a/tests/test_cell_ref_eval.py b/tests/test_cell_ref_eval.py
index 2c5e0b17..702c97fb 100644
--- a/tests/test_cell_ref_eval.py
+++ b/tests/test_cell_ref_eval.py
@@ -1,7 +1,7 @@
 import numpy as np
 from tabulous import TableViewer
 import pandas as pd
-from numpy.testing import assert_allclose
+from numpy.testing import assert_allclose, assert_equal
 import pytest
 
 def test_scalar():
@@ -246,3 +246,91 @@ def test_removing_or_inserting_left_column():
     assert sheet.cell[0, 2] == "10.0"
     sheet.cell[1, 0] = 5
     assert sheet.cell[0, 2] == "10.0"
+
+def test_eval_during_sort():
+    viewer = TableViewer(show=False)
+    rng = np.random.default_rng(123)
+    sheet = viewer.add_spreadsheet(rng.poisson(3, size=(5, 1)))
+    sheet.proxy.set([1, 3, 2, 4, 0])
+    sheet.cell[1, 1] = "&=np.sum(df.iloc[:, 0])"
+    assert sheet.data_shown.iloc[1, 1] == np.sum(sheet.data.iloc[:, 0])
+    sheet.proxy.reset()
+    assert sheet.data_shown.iloc[3, 1] == np.sum(sheet.data.iloc[:, 0])
+    assert (3, 1) in sheet.cell.ref
+
+def test_eval_during_filter():
+    viewer = TableViewer(show=False)
+    rng = np.random.default_rng(123)
+    sheet = viewer.add_spreadsheet({"a": [0, 1, 1, 0, 1], "b": rng.poisson(3, size=5)})
+    sheet.proxy.filter("a == 1")
+    sheet.cell[0, 2] = "&=np.sum(df.iloc[:, 1])"
+    assert sheet.data_shown.iloc[0, 2] == np.sum(sheet.data.iloc[:, 1])
+    sheet.proxy.reset()
+    assert sheet.data_shown.iloc[1, 2] == np.sum(sheet.data.iloc[:, 1])
+    assert (1, 2) in sheet.cell.ref
+
+def test_broadcasting_during_sort():
+    viewer = TableViewer(show=False)
+    rng = np.random.default_rng(123)
+    sheet = viewer.add_spreadsheet(rng.poisson(3, size=(5, 1)))
+    order = [1, 3, 2, 4, 0]
+    sheet.proxy.set(order)
+    sheet.cell[1, 1] = "&=np.cumsum(df.iloc[:, 0])"
+    assert_equal(sheet.data_shown.iloc[:, 1].values, np.cumsum(sheet.data.iloc[:, 0])[order].values)
+    sheet.proxy.reset()
+    assert_equal(sheet.data_shown.iloc[:, 1].values, np.cumsum(sheet.data.iloc[:, 0]).values)
+    assert (3, 1) in sheet.cell.ref
+
+def test_broadcasting_during_filter():
+    viewer = TableViewer(show=False)
+    rng = np.random.default_rng(123)
+    sheet = viewer.add_spreadsheet({"a": [0, 1, 1, 0, 1], "b": rng.poisson(3, size=5)})
+    sheet.proxy.filter("a == 1")
+    idx = sheet.proxy.as_indexer()
+    sheet.cell[0, 2] = "&=np.cumsum(df.iloc[:, 1])"
+    assert_equal(sheet.data_shown.iloc[:, 2].values, np.cumsum(sheet.data.iloc[:, 1])[idx].values)
+    sheet.proxy.reset()
+    assert_equal(sheet.data_shown.iloc[:, 2].values, np.cumsum(sheet.data.iloc[:, 1]).values)
+    assert (1, 2) in sheet.cell.ref
+
+def _assert_status_equal(s: str, ref: str):
+    l = len("<b><code>")
+    r = len("</code></b>")
+    assert s[l:-r] == ref
+
+def test_status_tip():
+    viewer = TableViewer(show=False)
+    sheet = viewer.add_spreadsheet(np.zeros((3, 2)))
+    sheet.cell[0, 1] = "&=np.sum(df.iloc[:, 0])"
+    sheet.cell[0, 2] = "&=np.sin(df.iloc[:, 0])"
+    sheet.move_iloc(0, 0)
+    _assert_status_equal(viewer.status, "")
+    sheet.move_iloc(0, 1)
+    _assert_status_equal(viewer.status, "df.iloc[0:1, 1:2] = np.sum(df.iloc[0:3, 0])")
+    sheet.move_iloc(1, 1)
+    _assert_status_equal(viewer.status, "")
+    sheet.move_iloc(0, 2)
+    _assert_status_equal(viewer.status, "df.iloc[0:3, 2:3] = np.sin(df.iloc[0:3, 0])")
+    sheet.move_iloc(1, 2)
+    _assert_status_equal(viewer.status, "df.iloc[0:3, 2:3] = np.sin(df.iloc[0:3, 0])")
+    sheet.move_iloc(2, 2)
+    _assert_status_equal(viewer.status, "df.iloc[0:3, 2:3] = np.sin(df.iloc[0:3, 0])")
+
+def test_status_tip_with_proxy():
+    viewer = TableViewer(show=False)
+    sheet = viewer.add_spreadsheet(np.zeros((5, 2)))
+    sheet.cell[1, 1] = "&=np.sum(df.iloc[:, 0])"
+    sheet.cell[0, 2] = "&=np.sin(df.iloc[:, 0])"
+    sheet.proxy.set([False, True, True, False, True])
+    sheet.move_iloc(0, 0)
+    _assert_status_equal(viewer.status, "")
+    sheet.move_iloc(0, 1)
+    _assert_status_equal(viewer.status, "df.iloc[1:2, 1:2] = np.sum(df.iloc[0:5, 0])")
+    sheet.move_iloc(1, 1)
+    _assert_status_equal(viewer.status, "")
+    sheet.move_iloc(0, 2)
+    _assert_status_equal(viewer.status, "df.iloc[0:5, 2:3] = np.sin(df.iloc[0:5, 0])")
+    sheet.move_iloc(1, 2)
+    _assert_status_equal(viewer.status, "df.iloc[0:5, 2:3] = np.sin(df.iloc[0:5, 0])")
+    sheet.move_iloc(2, 2)
+    _assert_status_equal(viewer.status, "df.iloc[0:5, 2:3] = np.sin(df.iloc[0:5, 0])")