shortcutxl 0.2.13 → 0.2.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -7
- package/dist/core/keybindings.js +3 -1
- package/dist/core/settings-manager.js +29 -0
- package/dist/custom/install-utils.js +0 -3
- package/dist/custom/prompts/api.js +1 -1
- package/dist/custom/providers/provider-ids.js +1 -0
- package/dist/custom/tools/excel-approval.js +180 -0
- package/dist/custom/tools/excel-exec.js +66 -11
- package/dist/custom/tools/task/render.js +1 -1
- package/dist/main.js +12 -1
- package/dist/modes/interactive/components/footer.js +10 -0
- package/dist/modes/interactive/components/settings-selector.js +11 -0
- package/dist/modes/interactive/interactive-mode.js +29 -5
- package/package.json +5 -3
- package/xll/ShortcutXL.xll +0 -0
- package/xll/modules/shortcut_xl/__init__.py +29 -14
- package/xll/modules/shortcut_xl/_com.py +1 -0
- package/xll/modules/shortcut_xl/_diff_highlight.py +133 -91
- package/xll/modules/shortcut_xl/_exec_entry.py +191 -0
- package/xll/modules/shortcut_xl/_log.py +1 -1
- package/xll/modules/shortcut_xl/_managed.py +15 -9
- package/xll/modules/shortcut_xl/_navigate.py +115 -0
- package/xll/modules/shortcut_xl/_threading.py +4 -3
- package/xll/modules/shortcut_xl/_tracking.py +15 -3
- package/xll/modules/shortcut_xl/api/__init__.py +2 -2
- package/xll/modules/shortcut_xl/api/format.py +10 -5
- package/xll/modules/shortcut_xl/api/range_formatter.py +4 -4
- package/xll/modules/shortcut_xl/api/workbook.py +3 -8
- package/xll/modules/shortcut_xl/api/worksheet.py +7 -7
- package/xll/modules/shortcut_xl/api-reference.py +3 -0
- /package/skills/{COM-advanced-api → com-advanced-api}/SKILL.md +0 -0
- /package/skills/{COM-advanced-api → com-advanced-api}/excel-type-library.py +0 -0
- /package/skills/{COM-advanced-api → com-advanced-api}/office-type-library.py +0 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Navigate Excel to a specific cell — used during diff review.
|
|
2
|
+
|
|
3
|
+
Highlights the cursor cell with a colored border via direct Range
|
|
4
|
+
formatting. Visible even when Excel doesn't have focus (unlike
|
|
5
|
+
.Select() which only shows when Excel is foreground).
|
|
6
|
+
|
|
7
|
+
Thread safety: called via _run_on_main, serialized by s_exec_lock.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from shortcut_xl._log import xl_log
|
|
11
|
+
from shortcut_xl.api.utils.helpers import extract_sheet_and_range
|
|
12
|
+
|
|
13
|
+
# Excel COM constants
|
|
14
|
+
_XL_EDGE_LEFT = 7
|
|
15
|
+
_XL_EDGE_TOP = 8
|
|
16
|
+
_XL_EDGE_BOTTOM = 9
|
|
17
|
+
_XL_EDGE_RIGHT = 10
|
|
18
|
+
_XL_CONTINUOUS = 1
|
|
19
|
+
_XL_LINE_STYLE_NONE = -4142
|
|
20
|
+
_XL_MEDIUM = -4138
|
|
21
|
+
|
|
22
|
+
_CURSOR_BLUE = 0xFF0000 # BGR — distinct from yellow change highlights
|
|
23
|
+
_BORDER_EDGES = (_XL_EDGE_LEFT, _XL_EDGE_TOP, _XL_EDGE_BOTTOM, _XL_EDGE_RIGHT)
|
|
24
|
+
|
|
25
|
+
# Current cursor state — at most one cell highlighted at a time.
|
|
26
|
+
_cursor_ws = None # COM Worksheet of the current cursor cell
|
|
27
|
+
_cursor_address = None # e.g. "B5"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def parse_cell_ref(cell_ref):
|
|
31
|
+
"""Parse a cell reference into (workbook, sheet, address).
|
|
32
|
+
|
|
33
|
+
Supports single cells and ranges:
|
|
34
|
+
"[Book1]Sheet1!A1" → ("Book1", "Sheet1", "A1")
|
|
35
|
+
"Sheet1!A1:E1" → (None, "Sheet1", "A1:E1")
|
|
36
|
+
|
|
37
|
+
Returns (None, None, None) if the reference is invalid.
|
|
38
|
+
"""
|
|
39
|
+
rest = cell_ref
|
|
40
|
+
workbook = None
|
|
41
|
+
|
|
42
|
+
if rest.startswith('['):
|
|
43
|
+
close = rest.find(']')
|
|
44
|
+
if close <= 1: # missing or empty workbook name
|
|
45
|
+
return None, None, None
|
|
46
|
+
workbook = rest[1:close]
|
|
47
|
+
rest = rest[close + 1:]
|
|
48
|
+
|
|
49
|
+
sheet, address = extract_sheet_and_range(rest)
|
|
50
|
+
if not sheet or not address:
|
|
51
|
+
return None, None, None
|
|
52
|
+
|
|
53
|
+
return workbook, sheet, address
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _resolve_worksheet(app, workbook, sheet):
|
|
57
|
+
"""Get the COM Worksheet object for the given workbook/sheet names."""
|
|
58
|
+
if workbook:
|
|
59
|
+
return app.Workbooks(workbook).Worksheets(sheet)
|
|
60
|
+
return app.Worksheets(sheet)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _set_border(rng, color, line_style, weight):
|
|
64
|
+
"""Apply border styling to all four edges of a range."""
|
|
65
|
+
for edge in _BORDER_EDGES:
|
|
66
|
+
b = rng.Borders(edge)
|
|
67
|
+
b.LineStyle = line_style
|
|
68
|
+
if line_style != _XL_LINE_STYLE_NONE:
|
|
69
|
+
b.Color = color
|
|
70
|
+
b.Weight = weight
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _clear_cursor():
|
|
74
|
+
"""Remove the border from the current cursor cell, if any."""
|
|
75
|
+
global _cursor_ws, _cursor_address
|
|
76
|
+
if _cursor_ws is None or _cursor_address is None:
|
|
77
|
+
return
|
|
78
|
+
try:
|
|
79
|
+
_set_border(_cursor_ws.Range(_cursor_address), 0, _XL_LINE_STYLE_NONE, 0)
|
|
80
|
+
except Exception as e:
|
|
81
|
+
xl_log(f"_clear_cursor({_cursor_address}): {e}")
|
|
82
|
+
_cursor_ws = None
|
|
83
|
+
_cursor_address = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def navigate_to_cell(app, cell_ref):
|
|
87
|
+
"""Highlight the cell with a colored border.
|
|
88
|
+
|
|
89
|
+
*cell_ref* is "[Book1]Sheet1!A1" or "Sheet1!A1".
|
|
90
|
+
Clears the previous cursor border before applying the new one.
|
|
91
|
+
Returns True on success, False on failure.
|
|
92
|
+
"""
|
|
93
|
+
global _cursor_ws, _cursor_address
|
|
94
|
+
|
|
95
|
+
workbook, sheet, address = parse_cell_ref(cell_ref)
|
|
96
|
+
if not sheet or not address:
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
_clear_cursor()
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
ws = _resolve_worksheet(app, workbook, sheet)
|
|
103
|
+
rng = ws.Range(address)
|
|
104
|
+
_set_border(rng, _CURSOR_BLUE, _XL_CONTINUOUS, _XL_MEDIUM)
|
|
105
|
+
_cursor_ws = ws
|
|
106
|
+
_cursor_address = address
|
|
107
|
+
return True
|
|
108
|
+
except Exception as e:
|
|
109
|
+
xl_log(f"navigate_to_cell({cell_ref}): {e}")
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def clear_navigate_cursor():
|
|
114
|
+
"""Remove the cursor border. Called on approval accept/reject."""
|
|
115
|
+
_clear_cursor()
|
|
@@ -6,12 +6,12 @@ This matches the pattern used by PyXLL, Excel-DNA, and xlwings:
|
|
|
6
6
|
background threads enqueue work, a timer on the main thread drains it.
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
|
-
import threading
|
|
10
|
-
import queue
|
|
11
9
|
import ctypes
|
|
10
|
+
import queue
|
|
11
|
+
import threading
|
|
12
12
|
from ctypes import wintypes
|
|
13
|
-
from shortcut_xl._log import xl_log
|
|
14
13
|
|
|
14
|
+
from shortcut_xl._log import xl_log
|
|
15
15
|
|
|
16
16
|
# COM error code: Excel is busy (VBA_E_IGNORE = 0x800AC472)
|
|
17
17
|
_VBA_E_IGNORE = -2146777998
|
|
@@ -44,6 +44,7 @@ def _is_excel_busy(e):
|
|
|
44
44
|
|
|
45
45
|
# Persist main thread ID in builtins so it survives hot-reload.
|
|
46
46
|
import builtins
|
|
47
|
+
|
|
47
48
|
_main_thread_id = getattr(builtins, '_shortcutxl_main_thread_id', None)
|
|
48
49
|
if _main_thread_id is None:
|
|
49
50
|
_main_thread_id = threading.current_thread().ident
|
|
@@ -7,9 +7,9 @@ sheets) not O(workbook), and the agent gets raw COM objects for everything
|
|
|
7
7
|
below the Worksheet level — zero overhead on Range reads/writes.
|
|
8
8
|
"""
|
|
9
9
|
|
|
10
|
-
from shortcut_xl.api.utils.helpers import col_letter
|
|
11
|
-
from shortcut_xl.api.utils.com_utils import normalize_2d
|
|
12
10
|
from shortcut_xl._log import xl_log
|
|
11
|
+
from shortcut_xl.api.utils.com_utils import normalize_2d
|
|
12
|
+
from shortcut_xl.api.utils.helpers import col_letter
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
class _SheetSnapshot:
|
|
@@ -69,6 +69,9 @@ def _diff_snapshots(sheet_name, before, after):
|
|
|
69
69
|
formula = _get_formula(after, ri, ci)
|
|
70
70
|
if formula:
|
|
71
71
|
entry['formula'] = formula
|
|
72
|
+
old_formula = _get_formula(before, ri, ci)
|
|
73
|
+
if old_formula:
|
|
74
|
+
entry['oldFormula'] = old_formula
|
|
72
75
|
changes.append(entry)
|
|
73
76
|
return changes
|
|
74
77
|
|
|
@@ -105,6 +108,9 @@ def _diff_snapshots(sheet_name, before, after):
|
|
|
105
108
|
formula = _get_formula_abs(after, row, col)
|
|
106
109
|
if formula:
|
|
107
110
|
entry['formula'] = formula
|
|
111
|
+
old_formula = _get_formula_abs(before, row, col)
|
|
112
|
+
if old_formula:
|
|
113
|
+
entry['oldFormula'] = old_formula
|
|
108
114
|
changes.append(entry)
|
|
109
115
|
return changes
|
|
110
116
|
|
|
@@ -136,8 +142,14 @@ class DirtyTracker:
|
|
|
136
142
|
try:
|
|
137
143
|
after = _SheetSnapshot(ws)
|
|
138
144
|
changes = _diff_snapshots(name, before, after)
|
|
139
|
-
# Enrich with
|
|
145
|
+
# Enrich with workbook name + numberFormat
|
|
146
|
+
try:
|
|
147
|
+
wb_name = ws.Parent.Name
|
|
148
|
+
except Exception:
|
|
149
|
+
wb_name = None
|
|
140
150
|
for entry in changes:
|
|
151
|
+
if wb_name:
|
|
152
|
+
entry['workbook'] = wb_name
|
|
141
153
|
try:
|
|
142
154
|
fmt = ws.Range(entry['address']).NumberFormat
|
|
143
155
|
if fmt and fmt != 'General':
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
"""API layer — Workbook/Worksheet wrappers and supporting modules."""
|
|
2
2
|
|
|
3
|
-
from shortcut_xl.api.workbook import Workbook
|
|
4
|
-
from shortcut_xl.api.worksheet import Worksheet
|
|
3
|
+
from shortcut_xl.api.workbook import Workbook as Workbook
|
|
4
|
+
from shortcut_xl.api.worksheet import Worksheet as Worksheet
|
|
@@ -5,14 +5,19 @@ with Good / Issues breakdown and row sampling.
|
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import re
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
from shortcut_xl.api.categorize import (
|
|
10
|
+
GOOD,
|
|
11
|
+
HARDCODED_NUMBER,
|
|
12
|
+
HARDCODED_NUMBER_IN_FORMULA,
|
|
13
|
+
INVALID_FORMULA,
|
|
14
|
+
LARGE_PERCENTAGE,
|
|
10
15
|
categorize_cells,
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
is_com_error, com_error_to_str,
|
|
16
|
+
com_error_to_str,
|
|
17
|
+
is_com_error,
|
|
14
18
|
)
|
|
15
19
|
from shortcut_xl.api.utils.helpers import col_letter
|
|
20
|
+
from shortcut_xl.api.utils.numerical import NUMERIC_TYPES
|
|
16
21
|
|
|
17
22
|
# Display constants
|
|
18
23
|
MAX_ROWS_TO_SHOW = 10
|
|
@@ -165,7 +170,7 @@ def _format_row(sheet, row, entries, indent=2, show_issues=True, show_count=True
|
|
|
165
170
|
lines.append(f'{ind}{sheet}!Row {row + 1} ({col_range}){count_suffix}')
|
|
166
171
|
|
|
167
172
|
samples = _sample_first_last(entries, MAX_SAMPLES_PER_ROW)
|
|
168
|
-
for cell,
|
|
173
|
+
for cell, _issues, _col in samples:
|
|
169
174
|
fmt = cell.get('numberFormat')
|
|
170
175
|
old_str = format_value(cell.get('oldValue'), fmt)
|
|
171
176
|
new_str = _display_value(cell)
|
|
@@ -9,11 +9,11 @@ Replicates the TypeScript processCellRange / RangeFormatter logic:
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
-
from shortcut_xl.api.utils.helpers import index_to_address
|
|
13
12
|
from shortcut_xl.api.style import read_style
|
|
14
|
-
from shortcut_xl.api.utils.
|
|
13
|
+
from shortcut_xl.api.utils.helpers import index_to_address
|
|
14
|
+
from shortcut_xl.api.utils.numerical import NUMERIC_TYPES, is_numerical
|
|
15
15
|
from shortcut_xl.api.utils.ranges import consolidate_ranges
|
|
16
|
-
from shortcut_xl.api.utils.style_utils import
|
|
16
|
+
from shortcut_xl.api.utils.style_utils import get_style_description, get_style_key
|
|
17
17
|
|
|
18
18
|
# ---------------------------------------------------------------------------
|
|
19
19
|
# Constants (match TypeScript range-processing/types.ts)
|
|
@@ -349,8 +349,8 @@ def format_cell_range(
|
|
|
349
349
|
5. Context from cells to the left
|
|
350
350
|
6. Context from cells above
|
|
351
351
|
"""
|
|
352
|
+
from shortcut_xl.api.utils.com_utils import make_range, normalize_2d
|
|
352
353
|
from shortcut_xl.api.utils.helpers import parse_range
|
|
353
|
-
from shortcut_xl.api.utils.com_utils import normalize_2d, make_range
|
|
354
354
|
|
|
355
355
|
sr, sc, er, ec = parse_range(range_addr)
|
|
356
356
|
rows = er - sr + 1
|
|
@@ -5,17 +5,14 @@ Output formats match the SpreadJS equivalents.
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
-
from
|
|
9
|
-
|
|
8
|
+
from shortcut_xl.api.categorize import com_error_to_str, is_com_error
|
|
9
|
+
from shortcut_xl.api.named_ranges import get_named_range_info
|
|
10
10
|
from shortcut_xl.api.utils.helpers import (
|
|
11
|
-
|
|
11
|
+
extract_sheet_and_range,
|
|
12
12
|
index_to_address,
|
|
13
13
|
is_error_value,
|
|
14
14
|
parse_range,
|
|
15
|
-
extract_sheet_and_range,
|
|
16
15
|
)
|
|
17
|
-
from shortcut_xl.api.categorize import is_com_error, com_error_to_str
|
|
18
|
-
from shortcut_xl.api.named_ranges import get_named_range_info
|
|
19
16
|
from shortcut_xl.api.worksheet import Worksheet
|
|
20
17
|
|
|
21
18
|
# Error check constants (match SpreadJS error-checker.ts)
|
|
@@ -263,8 +260,6 @@ class Workbook:
|
|
|
263
260
|
rng.Value = com_error_to_str(data)
|
|
264
261
|
return
|
|
265
262
|
# Multi-cell range
|
|
266
|
-
rows = rng.Rows.Count
|
|
267
|
-
cols = rng.Columns.Count
|
|
268
263
|
for r_idx, row in enumerate(data):
|
|
269
264
|
if not isinstance(row, tuple):
|
|
270
265
|
row = (row,)
|
|
@@ -11,17 +11,17 @@ import json
|
|
|
11
11
|
import re
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
-
from shortcut_xl.
|
|
15
|
-
from shortcut_xl.api.
|
|
14
|
+
from shortcut_xl._log import xl_log
|
|
15
|
+
from shortcut_xl.api.categorize import com_error_to_str, is_com_error
|
|
16
|
+
from shortcut_xl.api.picture import add_picture
|
|
17
|
+
from shortcut_xl.api.range_formatter import format_cell_range
|
|
18
|
+
from shortcut_xl.api.style import read_style
|
|
19
|
+
from shortcut_xl.api.utils.com_utils import make_range, normalize_2d
|
|
16
20
|
from shortcut_xl.api.utils.helpers import (
|
|
17
21
|
index_to_address,
|
|
18
22
|
parse_range,
|
|
19
23
|
)
|
|
20
|
-
from shortcut_xl.api.utils.
|
|
21
|
-
from shortcut_xl._log import xl_log
|
|
22
|
-
from shortcut_xl.api.range_formatter import format_cell_range
|
|
23
|
-
from shortcut_xl.api.style import read_style
|
|
24
|
-
from shortcut_xl.api.picture import add_picture
|
|
24
|
+
from shortcut_xl.api.utils.numerical import NUMERIC_TYPES
|
|
25
25
|
|
|
26
26
|
# Excel xlAutoFillType enum values
|
|
27
27
|
_XL_FILL_DEFAULT = 0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|