execsql2 2.26.0__py3-none-any.whl → 2.28.0__py3-none-any.whl
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.
- execsql/cli/commands/__init__.py +1 -0
- execsql/cli/commands/format.py +2 -1
- execsql/cli/commands/listing.py +32 -5
- execsql/cli/commands/lsp.py +64 -0
- execsql/cli/dispatch.py +1 -1
- execsql/cli/help.py +44 -96
- execsql/cli/lint.py +58 -22
- execsql/db/access.py +4 -4
- execsql/db/base.py +17 -3
- execsql/db/dsn.py +2 -2
- execsql/db/duckdb.py +14 -2
- execsql/db/firebird.py +2 -3
- execsql/db/mysql.py +2 -2
- execsql/db/oracle.py +5 -5
- execsql/db/postgres.py +2 -3
- execsql/db/sqlserver.py +2 -2
- execsql/exporters/duckdb.py +2 -2
- execsql/exporters/ods.py +2 -2
- execsql/exporters/templates.py +2 -2
- execsql/exporters/xls.py +3 -3
- execsql/exporters/xlsx.py +2 -2
- execsql/exporters/yaml.py +3 -4
- execsql/format.py +71 -21
- execsql/importers/base.py +6 -10
- execsql/lsp/__init__.py +6 -0
- execsql/lsp/code_actions.py +217 -0
- execsql/lsp/completion.py +203 -0
- execsql/lsp/diagnostics.py +66 -0
- execsql/lsp/document.py +210 -0
- execsql/lsp/folding.py +59 -0
- execsql/lsp/formatting.py +37 -0
- execsql/lsp/hover.py +119 -0
- execsql/lsp/navigation.py +169 -0
- execsql/lsp/rename.py +91 -0
- execsql/lsp/server.py +358 -0
- execsql/metacommands/debug.py +2 -2
- execsql/metacommands/dispatch.py +7 -0
- execsql/metacommands/io_fileops.py +6 -6
- execsql/metacommands/reference.py +1404 -0
- execsql/metacommands/upsert.py +2 -5
- execsql/models.py +11 -0
- execsql/utils/errors.py +16 -0
- {execsql2-2.26.0.dist-info → execsql2-2.28.0.dist-info}/METADATA +34 -18
- {execsql2-2.26.0.dist-info → execsql2-2.28.0.dist-info}/RECORD +62 -49
- {execsql2-2.26.0.data → execsql2-2.28.0.data}/data/execsql2_extras/README.md +0 -0
- {execsql2-2.26.0.data → execsql2-2.28.0.data}/data/execsql2_extras/config_settings.sqlite +0 -0
- {execsql2-2.26.0.data → execsql2-2.28.0.data}/data/execsql2_extras/example_config_prompt.sql +0 -0
- {execsql2-2.26.0.data → execsql2-2.28.0.data}/data/execsql2_extras/make_config_db.sql +0 -0
- {execsql2-2.26.0.data → execsql2-2.28.0.data}/data/execsql2_extras/md_compare.sql +0 -0
- {execsql2-2.26.0.data → execsql2-2.28.0.data}/data/execsql2_extras/md_glossary.sql +0 -0
- {execsql2-2.26.0.data → execsql2-2.28.0.data}/data/execsql2_extras/md_upsert.sql +0 -0
- {execsql2-2.26.0.data → execsql2-2.28.0.data}/data/execsql2_extras/pg_compare.sql +0 -0
- {execsql2-2.26.0.data → execsql2-2.28.0.data}/data/execsql2_extras/pg_glossary.sql +0 -0
- {execsql2-2.26.0.data → execsql2-2.28.0.data}/data/execsql2_extras/pg_upsert.sql +0 -0
- {execsql2-2.26.0.data → execsql2-2.28.0.data}/data/execsql2_extras/script_template.sql +0 -0
- {execsql2-2.26.0.data → execsql2-2.28.0.data}/data/execsql2_extras/ss_compare.sql +0 -0
- {execsql2-2.26.0.data → execsql2-2.28.0.data}/data/execsql2_extras/ss_glossary.sql +0 -0
- {execsql2-2.26.0.data → execsql2-2.28.0.data}/data/execsql2_extras/ss_upsert.sql +0 -0
- {execsql2-2.26.0.dist-info → execsql2-2.28.0.dist-info}/WHEEL +0 -0
- {execsql2-2.26.0.dist-info → execsql2-2.28.0.dist-info}/entry_points.txt +0 -0
- {execsql2-2.26.0.dist-info → execsql2-2.28.0.dist-info}/licenses/LICENSE.txt +0 -0
- {execsql2-2.26.0.dist-info → execsql2-2.28.0.dist-info}/licenses/NOTICE +0 -0
execsql/lsp/hover.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Hover: what the metacommand, conditional test or variable under the cursor is.
|
|
2
|
+
|
|
3
|
+
Metacommands and conditional tests are explained from
|
|
4
|
+
:mod:`execsql.metacommands.reference`; a variable by where the script defines
|
|
5
|
+
it, or by its reference entry for system variables.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
from lsprotocol import types
|
|
13
|
+
|
|
14
|
+
from execsql.lsp.document import Reference, ScriptIndex, variable_at
|
|
15
|
+
from execsql.metacommands import reference
|
|
16
|
+
|
|
17
|
+
__all__ = ["hover", "metacommand_for"]
|
|
18
|
+
|
|
19
|
+
_RX_METACOMMAND = re.compile(r"^(?P<lead>\s*--\s*!x!\s*)(?P<command>.*?)\s*$", re.I)
|
|
20
|
+
_RX_WORD = re.compile(r"[A-Za-z_]+")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def metacommand_for(command: str) -> reference.Metacommand | None:
|
|
24
|
+
"""The reference entry for a metacommand's text (what follows ``-- !x!``).
|
|
25
|
+
|
|
26
|
+
The dispatch entry that would run it names its keyword when it has one
|
|
27
|
+
(``PROMPT MESSAGE "..." MAP ...`` is PROMPT MAP); otherwise the longest
|
|
28
|
+
keyword the text starts with (``AUTOCOMMIT ON`` is AUTOCOMMIT,
|
|
29
|
+
``CONFIG BOOLEAN_INT YES`` is CONFIG).
|
|
30
|
+
"""
|
|
31
|
+
from execsql.cli.lint import _dispatch_table
|
|
32
|
+
|
|
33
|
+
match = _dispatch_table().get_match(command)
|
|
34
|
+
if match is not None and match[0].description:
|
|
35
|
+
entry = reference.metacommand(match[0].description)
|
|
36
|
+
if entry is not None:
|
|
37
|
+
return entry
|
|
38
|
+
best = None
|
|
39
|
+
for entry in reference.metacommands():
|
|
40
|
+
if re.match(rf"{re.escape(entry.keyword)}S?\b", command.strip(), re.I) and (
|
|
41
|
+
best is None or len(entry.keyword) > len(best.keyword)
|
|
42
|
+
):
|
|
43
|
+
best = entry
|
|
44
|
+
return best
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _markdown(text: str) -> types.MarkupContent:
|
|
48
|
+
return types.MarkupContent(kind=types.MarkupKind.Markdown, value=text)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _range(line: int, start: int, end: int) -> types.Range:
|
|
52
|
+
return types.Range(start=types.Position(line=line, character=start), end=types.Position(line=line, character=end))
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _syntax_block(forms: tuple[str, ...]) -> str:
|
|
56
|
+
return "```\n" + "\n".join(forms) + "\n```"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _variable_hover(index: ScriptIndex, ref: Reference, lines: list[str]) -> str | None:
|
|
60
|
+
name = ref.name
|
|
61
|
+
if name.startswith("$"):
|
|
62
|
+
var = reference.system_variable(name)
|
|
63
|
+
if var is None:
|
|
64
|
+
return f"**{name}**: not a system variable execsql defines."
|
|
65
|
+
return f"**{name}** — system variable\n\n{var.summary}\n\n[Reference]({var.url})"
|
|
66
|
+
if name.startswith("&"):
|
|
67
|
+
return f"**{name}** — the environment variable `{name[1:]}`."
|
|
68
|
+
if name.startswith("@"):
|
|
69
|
+
return f"**{name}** — a column value assigned by SELECT_SUB or PROMPT SELECT_SUB."
|
|
70
|
+
if name.startswith("#"):
|
|
71
|
+
owners = [s for s, params in index.script_params.items() if name[1:].lower() in (p.lower() for p in params)]
|
|
72
|
+
if owners:
|
|
73
|
+
return f"**{name}** — parameter of SCRIPT {', '.join(owners)}."
|
|
74
|
+
return f"**{name}** — a SCRIPT parameter, but no SCRIPT here declares `{name[1:]}`."
|
|
75
|
+
locations = index.definitions.get(ref.key)
|
|
76
|
+
if not locations:
|
|
77
|
+
return f"**{name}** is not defined in this script or the files it includes (lint rule V001)."
|
|
78
|
+
parts = [f"**{name}** — substitution variable"]
|
|
79
|
+
for loc in locations[:5]:
|
|
80
|
+
where = f"line {loc.line + 1}" if loc.path == index.path else f"{loc.path}, line {loc.line + 1}"
|
|
81
|
+
text = lines[loc.line].strip() if loc.path == index.path and loc.line < len(lines) else ""
|
|
82
|
+
parts.append(f"Defined at {where}" + (f":\n```\n{text}\n```" if text else "."))
|
|
83
|
+
if len(locations) > 5:
|
|
84
|
+
parts.append(f"… and {len(locations) - 5} more.")
|
|
85
|
+
return "\n\n".join(parts)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def hover(index: ScriptIndex, source: str, line: int, character: int) -> types.Hover | None:
|
|
89
|
+
"""What to show for the cursor at (*line*, *character*), zero-based."""
|
|
90
|
+
lines = source.splitlines()
|
|
91
|
+
text = lines[line] if 0 <= line < len(lines) else ""
|
|
92
|
+
|
|
93
|
+
ref = variable_at(index, line, character)
|
|
94
|
+
if ref is not None:
|
|
95
|
+
content = _variable_hover(index, ref, lines)
|
|
96
|
+
loc = ref.location
|
|
97
|
+
return types.Hover(contents=_markdown(content), range=_range(line, loc.start, loc.end)) if content else None
|
|
98
|
+
|
|
99
|
+
m = _RX_METACOMMAND.match(text)
|
|
100
|
+
if not m or character < m.end("lead"):
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
# A conditional test under the cursor: a word followed by "(".
|
|
104
|
+
for word in _RX_WORD.finditer(text):
|
|
105
|
+
if word.start() <= character <= word.end() and text[word.end() :].lstrip().startswith("("):
|
|
106
|
+
cond = reference.condition(word.group())
|
|
107
|
+
if cond is not None and cond.name not in ("NOT", "OR"):
|
|
108
|
+
body = f"**{cond.name}** — conditional test\n\n{cond.summary}\n\n{_syntax_block(cond.forms)}\n\n[Reference]({cond.url})"
|
|
109
|
+
return types.Hover(contents=_markdown(body), range=_range(line, word.start(), word.end()))
|
|
110
|
+
|
|
111
|
+
entry = metacommand_for(m.group("command"))
|
|
112
|
+
if entry is None:
|
|
113
|
+
return None
|
|
114
|
+
start = m.end("lead")
|
|
115
|
+
written = re.match(r"\s*".join(map(re.escape, entry.keyword.split())), text[start:], re.I)
|
|
116
|
+
first_word = re.match(r"[A-Za-z_]+", text[start:])
|
|
117
|
+
keyword_end = start + (written.end() if written else first_word.end() if first_word else 0)
|
|
118
|
+
body = f"**{entry.keyword}** — {entry.category} metacommand\n\n{entry.summary}\n\n{_syntax_block(entry.forms)}\n\n[Reference]({entry.url})"
|
|
119
|
+
return types.Hover(contents=_markdown(body), range=_range(line, start, keyword_end))
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Navigation: go to definition, find references, the document outline and INCLUDE links."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from lsprotocol import types
|
|
8
|
+
|
|
9
|
+
from execsql.lsp.document import Location, ScriptIndex, file_uri, uri_for, variable_at
|
|
10
|
+
from execsql.script.ast import (
|
|
11
|
+
BatchBlock,
|
|
12
|
+
IfBlock,
|
|
13
|
+
IncludeDirective,
|
|
14
|
+
LoopBlock,
|
|
15
|
+
MetaCommandStatement,
|
|
16
|
+
Node,
|
|
17
|
+
ScriptBlock,
|
|
18
|
+
SqlBlock,
|
|
19
|
+
)
|
|
20
|
+
from execsql.script.parser import parse_string
|
|
21
|
+
|
|
22
|
+
__all__ = ["definition", "document_links", "document_symbols", "references"]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _range(line: int, start: int, end: int) -> types.Range:
|
|
26
|
+
return types.Range(start=types.Position(line=line, character=start), end=types.Position(line=line, character=end))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _lsp_location(loc: Location, index: ScriptIndex, uri: str) -> types.Location:
|
|
30
|
+
return types.Location(uri=uri_for(loc.path, index, uri), range=_range(loc.line, loc.start, loc.end))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _at(entries: list[tuple[Location, str]], line: int, character: int) -> str | None:
|
|
34
|
+
for loc, value in entries:
|
|
35
|
+
if loc.line == line and loc.start <= character <= loc.end:
|
|
36
|
+
return value
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def definition(index: ScriptIndex, uri: str, line: int, character: int) -> list[types.Location]:
|
|
41
|
+
"""Where the variable, SCRIPT or included file under the cursor is defined."""
|
|
42
|
+
ref = variable_at(index, line, character)
|
|
43
|
+
if ref is not None:
|
|
44
|
+
if ref.name.startswith("#"):
|
|
45
|
+
name = ref.name[1:].lower()
|
|
46
|
+
return [
|
|
47
|
+
_lsp_location(index.scripts[script], index, uri)
|
|
48
|
+
for script, params in index.script_params.items()
|
|
49
|
+
if name in (p.lower() for p in params) and script in index.scripts
|
|
50
|
+
]
|
|
51
|
+
return [_lsp_location(loc, index, uri) for loc in index.definitions.get(ref.key, [])]
|
|
52
|
+
|
|
53
|
+
script = _at(index.script_calls, line, character)
|
|
54
|
+
if script is not None and script in index.scripts:
|
|
55
|
+
return [_lsp_location(index.scripts[script], index, uri)]
|
|
56
|
+
|
|
57
|
+
target = _at(index.includes, line, character)
|
|
58
|
+
if target is not None and Path(target).is_file():
|
|
59
|
+
uri_of_file = file_uri(target, index)
|
|
60
|
+
if uri_of_file:
|
|
61
|
+
return [types.Location(uri=uri_of_file, range=_range(0, 0, 0))]
|
|
62
|
+
return []
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def references(
|
|
66
|
+
index: ScriptIndex,
|
|
67
|
+
uri: str,
|
|
68
|
+
line: int,
|
|
69
|
+
character: int,
|
|
70
|
+
include_declaration: bool = True,
|
|
71
|
+
) -> list[types.Location]:
|
|
72
|
+
"""Every use of the variable under the cursor in this file and the files it includes (and its definitions)."""
|
|
73
|
+
ref = variable_at(index, line, character)
|
|
74
|
+
if ref is None:
|
|
75
|
+
return []
|
|
76
|
+
found = [_lsp_location(r.location, index, uri) for r in index.references if r.key == ref.key]
|
|
77
|
+
if include_declaration and not ref.name.startswith(("$", "&", "@", "#")):
|
|
78
|
+
found += [_lsp_location(loc, index, uri) for loc in index.definitions.get(ref.key, [])]
|
|
79
|
+
return found
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def document_links(index: ScriptIndex) -> list[types.DocumentLink]:
|
|
83
|
+
"""Each INCLUDE target that exists, as a link to the file."""
|
|
84
|
+
links = []
|
|
85
|
+
for loc, target in index.includes:
|
|
86
|
+
path = Path(target)
|
|
87
|
+
if path.is_file():
|
|
88
|
+
uri = file_uri(target, index)
|
|
89
|
+
if uri:
|
|
90
|
+
links.append(types.DocumentLink(range=_range(loc.line, loc.start, loc.end), target=uri))
|
|
91
|
+
return links
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _node_range(node: Node, lines: list[str]) -> types.Range:
|
|
95
|
+
start = node.span.start_line - 1
|
|
96
|
+
end = node.span.effective_end_line - 1
|
|
97
|
+
end_text = lines[end] if 0 <= end < len(lines) else ""
|
|
98
|
+
return types.Range(
|
|
99
|
+
start=types.Position(line=start, character=0),
|
|
100
|
+
end=types.Position(line=end, character=len(end_text)),
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _symbol(
|
|
105
|
+
name: str,
|
|
106
|
+
kind: types.SymbolKind,
|
|
107
|
+
node: Node,
|
|
108
|
+
lines: list[str],
|
|
109
|
+
children: list[types.DocumentSymbol],
|
|
110
|
+
) -> types.DocumentSymbol:
|
|
111
|
+
rng = _node_range(node, lines)
|
|
112
|
+
head = types.Range(
|
|
113
|
+
start=rng.start,
|
|
114
|
+
end=types.Position(
|
|
115
|
+
line=rng.start.line,
|
|
116
|
+
character=len(lines[rng.start.line]) if rng.start.line < len(lines) else 0,
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
return types.DocumentSymbol(name=name, kind=kind, range=rng, selection_range=head, children=children or None)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _symbols(nodes: list[Node], lines: list[str], index: ScriptIndex) -> list[types.DocumentSymbol]:
|
|
123
|
+
out: list[types.DocumentSymbol] = []
|
|
124
|
+
for node in nodes:
|
|
125
|
+
children = _symbols(list(node.children()), lines, index)
|
|
126
|
+
if isinstance(node, ScriptBlock):
|
|
127
|
+
params = ", ".join(p.name for p in node.param_defs or [])
|
|
128
|
+
out.append(
|
|
129
|
+
_symbol(
|
|
130
|
+
f"SCRIPT {node.name}({params})" if params else f"SCRIPT {node.name}",
|
|
131
|
+
types.SymbolKind.Function,
|
|
132
|
+
node,
|
|
133
|
+
lines,
|
|
134
|
+
children,
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
elif isinstance(node, IfBlock):
|
|
138
|
+
out.append(_symbol(f"IF ({node.condition.strip()})", types.SymbolKind.Namespace, node, lines, children))
|
|
139
|
+
elif isinstance(node, LoopBlock):
|
|
140
|
+
out.append(
|
|
141
|
+
_symbol(
|
|
142
|
+
f"LOOP {node.loop_type} ({node.condition.strip()})",
|
|
143
|
+
types.SymbolKind.Namespace,
|
|
144
|
+
node,
|
|
145
|
+
lines,
|
|
146
|
+
children,
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
elif isinstance(node, BatchBlock):
|
|
150
|
+
out.append(_symbol("BATCH", types.SymbolKind.Namespace, node, lines, children))
|
|
151
|
+
elif isinstance(node, SqlBlock):
|
|
152
|
+
out.append(_symbol("SQL block", types.SymbolKind.Namespace, node, lines, children))
|
|
153
|
+
elif isinstance(node, IncludeDirective) and not node.is_execute_script:
|
|
154
|
+
out.append(_symbol(f"INCLUDE {node.target.strip()}", types.SymbolKind.File, node, lines, []))
|
|
155
|
+
elif isinstance(node, MetaCommandStatement):
|
|
156
|
+
row = node.span.start_line - 1
|
|
157
|
+
for key, locations in index.definitions.items():
|
|
158
|
+
for loc in locations:
|
|
159
|
+
if loc.path == index.path and loc.line == row:
|
|
160
|
+
out.append(_symbol(index.spellings[key], types.SymbolKind.Variable, node, lines, []))
|
|
161
|
+
else:
|
|
162
|
+
out.extend(children)
|
|
163
|
+
return out
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def document_symbols(index: ScriptIndex, source: str) -> list[types.DocumentSymbol]:
|
|
167
|
+
"""The outline: SCRIPT blocks, IF / LOOP / BATCH blocks, INCLUDE directives and variable definitions, nested."""
|
|
168
|
+
tree = parse_string(source, index.path or "<untitled>", errors=[])
|
|
169
|
+
return _symbols(tree.body, source.splitlines(), index)
|
execsql/lsp/rename.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Rename a user variable everywhere the index sees it: the script and the files it includes.
|
|
2
|
+
|
|
3
|
+
Scripts that ``INCLUDE`` this one are not known to the server, so their
|
|
4
|
+
uses of the variable are not renamed.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from lsprotocol import types
|
|
12
|
+
|
|
13
|
+
from execsql.lsp.document import Location, ScriptIndex, uri_for, variable_at
|
|
14
|
+
|
|
15
|
+
__all__ = ["RenameError", "prepare_rename", "rename"]
|
|
16
|
+
|
|
17
|
+
_RX_NAME = re.compile(r"^\w+$")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class RenameError(ValueError):
|
|
21
|
+
"""Why a rename cannot be done; the server shows the message."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _target(index: ScriptIndex, line: int, character: int) -> tuple[str, Location] | None:
|
|
25
|
+
"""The user variable under the cursor: its key and where its bare name is written."""
|
|
26
|
+
ref = variable_at(index, line, character)
|
|
27
|
+
if ref is not None:
|
|
28
|
+
bare = ref.name.lstrip("~+")
|
|
29
|
+
if not _RX_NAME.match(bare): # $system, &environment, @column or #parameter
|
|
30
|
+
return None
|
|
31
|
+
loc = ref.location
|
|
32
|
+
return ref.key, Location(loc.path, loc.line, loc.end - len(bare), loc.end)
|
|
33
|
+
for key, locations in index.definitions.items():
|
|
34
|
+
for loc in locations:
|
|
35
|
+
if loc.path == index.path and loc.line == line and loc.start <= character <= loc.end:
|
|
36
|
+
return key, loc
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _range(loc: Location) -> types.Range:
|
|
41
|
+
return types.Range(
|
|
42
|
+
start=types.Position(line=loc.line, character=loc.start),
|
|
43
|
+
end=types.Position(line=loc.line, character=loc.end),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def prepare_rename(index: ScriptIndex, line: int, character: int) -> types.Range | None:
|
|
48
|
+
"""The name to rename at the cursor, or ``None`` if there is nothing renameable there."""
|
|
49
|
+
target = _target(index, line, character)
|
|
50
|
+
return _range(target[1]) if target else None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def rename(index: ScriptIndex, uri: str, line: int, character: int, new_name: str) -> types.WorkspaceEdit | None:
|
|
54
|
+
"""Edits renaming the variable under the cursor to *new_name*.
|
|
55
|
+
|
|
56
|
+
Raises :class:`RenameError` when the rename would not be correct: an
|
|
57
|
+
invalid name, one already defined, a name used both as a local
|
|
58
|
+
(``~name``) and a global variable (execsql keeps those apart; the index
|
|
59
|
+
does not), or a definition whose name cannot be located on its line.
|
|
60
|
+
"""
|
|
61
|
+
target = _target(index, line, character)
|
|
62
|
+
if target is None:
|
|
63
|
+
return None
|
|
64
|
+
key = target[0]
|
|
65
|
+
if not _RX_NAME.match(new_name):
|
|
66
|
+
raise RenameError(f"{new_name!r} is not a variable name: use letters, digits and underscores.")
|
|
67
|
+
if new_name.upper() != key and new_name.upper() in index.definitions:
|
|
68
|
+
raise RenameError(f"{new_name} is already defined.")
|
|
69
|
+
|
|
70
|
+
uses = [r for r in index.references if r.key == key]
|
|
71
|
+
prefixes = {r.name[: len(r.name) - len(r.name.lstrip("~+"))] for r in uses}
|
|
72
|
+
if "~" in prefixes and len(prefixes) > 1:
|
|
73
|
+
raise RenameError(f"{key.lower()} is used both as a local (~) and a global variable; rename it by hand.")
|
|
74
|
+
|
|
75
|
+
spans: set[Location] = set()
|
|
76
|
+
for ref in uses:
|
|
77
|
+
bare = ref.name.lstrip("~+")
|
|
78
|
+
loc = ref.location
|
|
79
|
+
spans.add(Location(loc.path, loc.line, loc.end - len(bare), loc.end))
|
|
80
|
+
for loc in index.definitions.get(key, []):
|
|
81
|
+
if loc.end - loc.start != len(key):
|
|
82
|
+
where = f"{loc.path or 'this file'}, line {loc.line + 1}"
|
|
83
|
+
raise RenameError(f"Cannot find the name {key.lower()} in its definition ({where}); rename it by hand.")
|
|
84
|
+
spans.add(loc)
|
|
85
|
+
|
|
86
|
+
changes: dict[str, list[types.TextEdit]] = {}
|
|
87
|
+
for loc in sorted(spans, key=lambda s: (s.path or "", s.line, s.start)):
|
|
88
|
+
changes.setdefault(uri_for(loc.path, index, uri), []).append(
|
|
89
|
+
types.TextEdit(range=_range(loc), new_text=new_name),
|
|
90
|
+
)
|
|
91
|
+
return types.WorkspaceEdit(changes=changes)
|