forked from balisujohn/localwriter
-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathcell_discovery.py
More file actions
200 lines (170 loc) · 6.81 KB
/
Copy pathcell_discovery.py
File metadata and controls
200 lines (170 loc) · 6.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# WriterAgent - AI Writing Assistant for LibreOffice
# Copyright (c) 2026 KeithCu
#
# SPDX-License-Identifier: GPL-3.0-or-later
"""Discover ``=PY()`` / ``=PYTHON()`` formula cells on a Calc sheet.
Used by the LibrePy Python sidebar (cell list + click-to-navigate). Handles both
short formulas and fully qualified add-in forms from WriterAgent / LibrePy.
"""
from __future__ import annotations
import logging
import re
from dataclasses import dataclass
from typing import Any
from plugin.calc.address_utils import index_to_column
from plugin.calc.python.formula_edit import (
cell_looks_python_like,
extract_python_code_loose,
normalize_formula_string,
parse_python_formula,
)
log = logging.getLogger(__name__)
# LibreOffice stores registered add-ins as fully qualified names in getFormula().
# LibrePy's add-in still registers as writeragent.PythonFunction for formula compat;
# also accept a future librepy-prefixed form.
_ADDIN_PY_PREFIX_RE = re.compile(
r"^=\s*ORG\.EXTENSION\.(?:WRITERAGENT|LIBREPY)\.PYTHONFUNCTION\.(?:PYTHON|PY)\s*\(",
re.IGNORECASE,
)
# CellFlags.FORMULA = 16
_CELL_FLAG_FORMULA = 16
_MAX_PYTHON_CELLS_FOUND = 100
_MAX_CELLS_TO_SCAN = 50000
@dataclass(frozen=True)
class PythonCellInfo:
"""One discovered Python formula cell."""
sheet: str
row: int # 0-based
column: int # 0-based
address: str # e.g. "Sheet1.A1"
code: str
formula: str
def canonicalize_py_formula_for_parse(formula: str) -> str:
"""Map LO add-in formula text to ``=PYTHON(…)`` for ``parse_python_formula``."""
raw = normalize_formula_string(formula)
match = _ADDIN_PY_PREFIX_RE.match(raw)
if match:
return "=PYTHON(" + raw[match.end() :]
return raw
def is_py_formula_text(formula: str) -> bool:
"""True when *formula* is PY/PYTHON, including fully qualified add-in form."""
return cell_looks_python_like(canonicalize_py_formula_for_parse(formula))
def extract_code_from_formula(formula: str) -> str:
"""Best-effort Python source from a PY/PYTHON formula."""
canonical = canonicalize_py_formula_for_parse(formula)
parts = parse_python_formula(canonical)
if parts is not None:
return parts.code
return extract_python_code_loose(canonical) or ""
def _cell_address(sheet_name: str, row: int, column: int) -> str:
return f"{sheet_name}.{index_to_column(column)}{row + 1}"
def list_python_cells_on_sheet(sheet: Any, *, sheet_name: str | None = None) -> list[PythonCellInfo]:
"""Return Python formula cells on *sheet*, sorted by row then column."""
if sheet is None:
return []
name = sheet_name
if not name:
try:
name = str(sheet.getName() or "")
except Exception:
name = ""
if not name:
name = "Sheet"
found: list[PythonCellInfo] = []
try:
formula_cells = sheet.queryContentCells(_CELL_FLAG_FORMULA)
except Exception:
log.debug("list_python_cells_on_sheet: queryContentCells failed", exc_info=True)
return []
if formula_cells is None:
return []
try:
count = int(formula_cells.getCount())
except Exception:
return []
scanned_count = 0
for i in range(count):
if len(found) >= _MAX_PYTHON_CELLS_FOUND or scanned_count >= _MAX_CELLS_TO_SCAN:
break
try:
cell_range = formula_cells.getByIndex(i)
addr = cell_range.getRangeAddress()
formula_matrix = cell_range.getFormulas() if hasattr(cell_range, "getFormulas") else None
except Exception:
continue
if formula_matrix is not None and len(formula_matrix) > 0:
for r_idx, row_formulas in enumerate(formula_matrix):
row = addr.StartRow + r_idx
for c_idx, formula in enumerate(row_formulas):
scanned_count += 1
col = addr.StartColumn + c_idx
if not formula or not is_py_formula_text(str(formula)):
continue
code = extract_code_from_formula(str(formula))
found.append(
PythonCellInfo(
sheet=name,
row=row,
column=col,
address=_cell_address(name, row, col),
code=code,
formula=str(formula),
)
)
if len(found) >= _MAX_PYTHON_CELLS_FOUND:
break
if len(found) >= _MAX_PYTHON_CELLS_FOUND:
break
else:
for row in range(addr.StartRow, addr.EndRow + 1):
if len(found) >= _MAX_PYTHON_CELLS_FOUND or scanned_count >= _MAX_CELLS_TO_SCAN:
break
for col in range(addr.StartColumn, addr.EndColumn + 1):
scanned_count += 1
if scanned_count > _MAX_CELLS_TO_SCAN:
break
try:
cell = sheet.getCellByPosition(col, row)
formula = str(cell.getFormula() or "")
except Exception:
continue
if not is_py_formula_text(formula):
continue
code = extract_code_from_formula(formula)
found.append(
PythonCellInfo(
sheet=name,
row=row,
column=col,
address=_cell_address(name, row, col),
code=code,
formula=formula,
)
)
if len(found) >= _MAX_PYTHON_CELLS_FOUND:
break
found.sort(key=lambda c: (c.row, c.column))
return found
def list_python_cells_in_doc(doc: Any, *, active_sheet_only: bool = True) -> list[PythonCellInfo]:
"""Enumerate Python cells in *doc* (active sheet by default)."""
if doc is None:
return []
try:
controller = doc.getCurrentController()
if active_sheet_only and controller is not None:
sheet = controller.getActiveSheet()
if sheet is not None:
return list_python_cells_on_sheet(sheet)
except Exception:
log.debug("list_python_cells_in_doc: active sheet path failed", exc_info=True)
if active_sheet_only:
return []
out: list[PythonCellInfo] = []
try:
sheets = doc.getSheets()
for i in range(sheets.getCount()):
sheet = sheets.getByIndex(i)
out.extend(list_python_cells_on_sheet(sheet))
except Exception:
log.debug("list_python_cells_in_doc: all-sheets path failed", exc_info=True)
return out