|
13 | 13 |
|
14 | 14 | from .utils import ( |
15 | 15 | get_file_id, |
| 16 | + get_global_awareness, |
16 | 17 | get_jupyter_ydoc, |
17 | 18 | normalize_filepath, |
18 | 19 | ) |
@@ -862,6 +863,147 @@ def _safe_set_cursor( |
862 | 863 | # Cursor positioning is a visual enhancement, not critical functionality |
863 | 864 | pass |
864 | 865 |
|
| 866 | +async def get_active_notebook(username: Optional[str] = None) -> Optional[str]: |
| 867 | + """ |
| 868 | + Returns path for the currently active notebook. |
| 869 | + |
| 870 | + Args: |
| 871 | + username: Optional username to return a specific user's active notebook |
| 872 | + |
| 873 | + Returns: |
| 874 | + File path for the first active notebook. If username is provided, then |
| 875 | + returns the active notebook for that specific user. |
| 876 | + """ |
| 877 | + awareness = await get_global_awareness() |
| 878 | + if not awareness: |
| 879 | + return |
| 880 | + for _, state in awareness.states.items(): |
| 881 | + _username = state.get("user", {}).get("username", None) |
| 882 | + if(username and username != _username): |
| 883 | + continue |
| 884 | + |
| 885 | + if active_notebook := state.get("current"): |
| 886 | + return active_notebook.replace("notebook:", "") |
| 887 | + |
| 888 | +def _get_active_cell_id_from_ydoc(ydoc: YNotebook, username: Optional[str] = None) -> Optional[str]: |
| 889 | + """Internal helper: Returns the active cell id from a ydoc instance |
| 890 | +
|
| 891 | + Args: |
| 892 | + ydoc: The YNotebook instance |
| 893 | + username: Optional username to return a specific user's active cell |
| 894 | +
|
| 895 | + Returns: |
| 896 | + The active cell ID for the notebook, or None if no active cell found |
| 897 | + """ |
| 898 | + if not ydoc or not ydoc.awareness: |
| 899 | + return None |
| 900 | + |
| 901 | + awareness_states = ydoc.awareness.states |
| 902 | + for _, state in awareness_states.items(): |
| 903 | + _username = state.get("user", {}).get("username", None) |
| 904 | + if(username and username != _username): |
| 905 | + continue |
| 906 | + |
| 907 | + if active_cell_id := state.get("activeCellId"): |
| 908 | + return active_cell_id |
| 909 | + |
| 910 | + return None |
| 911 | + |
| 912 | + |
| 913 | +async def get_active_cell_id(notebook_path: str, username: Optional[str] = None) -> Optional[str]: |
| 914 | + """Returns the active cell id |
| 915 | +
|
| 916 | + Args: |
| 917 | + notebook_path: Path to the notebook file |
| 918 | + username: Optional username to return a specific user's active cell |
| 919 | +
|
| 920 | + Returns: |
| 921 | + The active cell ID for the notebook, or None if no active cell found |
| 922 | + """ |
| 923 | + file_path = normalize_filepath(notebook_path) |
| 924 | + file_id = await get_file_id(file_path) |
| 925 | + ydoc = await get_jupyter_ydoc(file_id) |
| 926 | + |
| 927 | + return _get_active_cell_id_from_ydoc(ydoc, username) |
| 928 | + |
| 929 | + |
| 930 | +async def select_cell(cell_id: str, username: Optional[str] = None) -> dict: |
| 931 | + """ |
| 932 | + Selects a cell in the active notebook by navigating to it using cursor movements. |
| 933 | +
|
| 934 | + This function finds the target cell by ID and navigates to it from the currently |
| 935 | + active cell using select_cell_above or select_cell_below commands. |
| 936 | +
|
| 937 | + Args: |
| 938 | + cell_id: The UUID of the cell to select, or a numeric index as string |
| 939 | + username: Optional username to get the active cell for that specific user |
| 940 | +
|
| 941 | + Returns: |
| 942 | + dict: A dictionary containing the response from the last cursor movement |
| 943 | +
|
| 944 | + Raises: |
| 945 | + ValueError: If the cell_id is not found in the notebook |
| 946 | + RuntimeError: If there is no active notebook or notebook is not currently open |
| 947 | + """ |
| 948 | + from jupyterlab_commands_toolkit.tools import execute_command |
| 949 | + |
| 950 | + try: |
| 951 | + # Get the active notebook path |
| 952 | + file_path = await get_active_notebook(username) |
| 953 | + if not file_path: |
| 954 | + raise RuntimeError( |
| 955 | + "No active notebook found. Please open a notebook first." |
| 956 | + ) |
| 957 | + |
| 958 | + # Resolve cell_id in case it's an index |
| 959 | + resolved_cell_id = await _resolve_cell_id(file_path, cell_id) |
| 960 | + |
| 961 | + # Get the YDoc for the notebook |
| 962 | + file_id = await get_file_id(file_path) |
| 963 | + ydoc = await get_jupyter_ydoc(file_id) |
| 964 | + |
| 965 | + if not ydoc: |
| 966 | + raise RuntimeError(f"Notebook at {file_path} is not currently open") |
| 967 | + |
| 968 | + # Get the target cell index |
| 969 | + target_cell_index = _get_cell_index_from_id_ydoc(ydoc, resolved_cell_id) |
| 970 | + if target_cell_index is None: |
| 971 | + raise ValueError(f"Cell with ID {cell_id} not found in notebook") |
| 972 | + |
| 973 | + # Get the currently active cell ID and index |
| 974 | + active_cell_id = _get_active_cell_id_from_ydoc(ydoc, username) |
| 975 | + if not active_cell_id: |
| 976 | + # If no active cell, we can't navigate - the notebook might need to be focused first |
| 977 | + raise RuntimeError( |
| 978 | + "No active cell found. Make sure the notebook is focused." |
| 979 | + ) |
| 980 | + |
| 981 | + active_cell_index = _get_cell_index_from_id_ydoc(ydoc, active_cell_id) |
| 982 | + if active_cell_index is None: |
| 983 | + raise RuntimeError(f"Active cell {active_cell_id} not found in notebook") |
| 984 | + |
| 985 | + # Calculate the distance and direction to move |
| 986 | + distance = target_cell_index - active_cell_index |
| 987 | + |
| 988 | + # If already at the target cell, no need to move |
| 989 | + if distance == 0: |
| 990 | + return {"success": True, "result": "Already at target cell"} |
| 991 | + |
| 992 | + # Navigate to the target cell |
| 993 | + result = None |
| 994 | + if distance > 0: |
| 995 | + # Move down |
| 996 | + for _ in range(distance): |
| 997 | + result = await execute_command("notebook:move-cursor-down") |
| 998 | + else: |
| 999 | + # Move up |
| 1000 | + for _ in range(abs(distance)): |
| 1001 | + result = await execute_command("notebook:move-cursor-up") |
| 1002 | + |
| 1003 | + return result |
| 1004 | + |
| 1005 | + except Exception: |
| 1006 | + raise |
865 | 1007 |
|
866 | 1008 | async def edit_cell(file_path: str, cell_id: str, content: str) -> None: |
867 | 1009 | """Edits the content of a notebook cell with the specified ID |
@@ -914,59 +1056,6 @@ async def edit_cell(file_path: str, cell_id: str, content: str) -> None: |
914 | 1056 | except Exception: |
915 | 1057 | raise |
916 | 1058 |
|
917 | | - |
918 | | -# Note: This is currently failing with server outputs, use `read_cell` instead |
919 | | -def read_cell_nbformat(file_path: str, cell_id: str) -> Dict[str, Any]: |
920 | | - """Returns the content and metadata of a cell with the specified ID. |
921 | | -
|
922 | | - This function reads a specific cell from a Jupyter notebook file using the nbformat |
923 | | - library and returns the cell's content and metadata. |
924 | | -
|
925 | | - Note: This function is currently not functioning properly with server outputs. |
926 | | - Use `read_cell` instead. |
927 | | -
|
928 | | - Args: |
929 | | - file_path: |
930 | | - The relative path to the notebook file on the filesystem. |
931 | | - cell_id: |
932 | | - The UUID of the cell to read. |
933 | | -
|
934 | | - Returns: |
935 | | - The cell as a dictionary containing its content and metadata. |
936 | | -
|
937 | | - Raises: |
938 | | - ValueError: If no cell with the given ID is found. |
939 | | - """ |
940 | | - file_path = normalize_filepath(file_path) |
941 | | - with open(file_path, "r", encoding="utf-8") as f: |
942 | | - notebook = nbformat.read(f, as_version=nbformat.NO_CONVERT) |
943 | | - |
944 | | - cell_index = _get_cell_index_from_id_nbformat(notebook, cell_id) |
945 | | - if cell_index is not None: |
946 | | - cell = notebook.cells[cell_index] |
947 | | - return cell |
948 | | - else: |
949 | | - raise ValueError(f"Cell with {cell_id=} not found in notebook at {file_path=}") |
950 | | - |
951 | | - |
952 | | -def _get_cell_index_from_id_json(notebook_json, cell_id: str) -> int | None: |
953 | | - """Get cell index from cell_id by notebook json dict. |
954 | | -
|
955 | | - Args: |
956 | | - notebook_json: |
957 | | - The notebook as a JSON dictionary. |
958 | | - cell_id: |
959 | | - The UUID of the cell to find. |
960 | | -
|
961 | | - Returns: |
962 | | - The index of the cell in the notebook, or None if not found. |
963 | | - """ |
964 | | - for i, cell in enumerate(notebook_json["cells"]): |
965 | | - if "id" in cell and cell["id"] == cell_id: |
966 | | - return i |
967 | | - return None |
968 | | - |
969 | | - |
970 | 1059 | def _get_cell_index_from_id_ydoc(ydoc, cell_id: str) -> int | None: |
971 | 1060 | """Get cell index from cell_id using YDoc interface. |
972 | 1061 |
|
@@ -1084,4 +1173,7 @@ async def create_notebook(file_path: str) -> str: |
1084 | 1173 | delete_cell, |
1085 | 1174 | edit_cell, |
1086 | 1175 | create_notebook, |
| 1176 | + get_active_notebook, |
| 1177 | + get_active_cell_id, |
| 1178 | + select_cell |
1087 | 1179 | ] |
0 commit comments