Skip to content

Commit d994ef5

Browse files
authored
feat: tag auto rename for cm (#2148)
1 parent b0e7fda commit d994ef5

36 files changed

Lines changed: 297 additions & 30 deletions

src/cm/tagAutoRename.ts

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { syntaxTree } from "@codemirror/language";
2+
import type { Extension, Text } from "@codemirror/state";
3+
import { Annotation, EditorState, Transaction } from "@codemirror/state";
4+
import type { ChangeSpec } from "@codemirror/state";
5+
import type { SyntaxNode } from "@lezer/common";
6+
7+
const skipTagAutoRename = Annotation.define<boolean>();
8+
const tagNamePattern = /^[^\s<>/="'`]+$/;
9+
10+
interface RenameTarget {
11+
tagName: SyntaxNode;
12+
pairedTagName: SyntaxNode;
13+
}
14+
15+
interface ChangedRange {
16+
fromA: number;
17+
toA: number;
18+
}
19+
20+
function getTagNameNode(state: EditorState, from: number, to: number): SyntaxNode | null {
21+
const tree = syntaxTree(state);
22+
const positions = from === to ? [from, from - 1] : [from, to, to - 1];
23+
24+
for (const pos of positions) {
25+
if (pos < 0 || pos > state.doc.length) continue;
26+
27+
for (const assoc of [-1, 1] as const) {
28+
let node: SyntaxNode | null = tree.resolveInner(pos, assoc);
29+
for (; node; node = node.parent) {
30+
if (node.name === "TagName") return node;
31+
if (
32+
node.name === "OpenTag" ||
33+
node.name === "CloseTag" ||
34+
node.name === "SelfClosingTag" ||
35+
node.name === "Element" ||
36+
node.type.isTop
37+
) {
38+
break;
39+
}
40+
}
41+
}
42+
}
43+
44+
return null;
45+
}
46+
47+
function readNode(doc: Text, node: SyntaxNode): string {
48+
return doc.sliceString(node.from, node.to);
49+
}
50+
51+
function getRenameTarget(
52+
state: EditorState,
53+
from: number,
54+
to: number,
55+
): RenameTarget | null {
56+
const tagName = getTagNameNode(state, from, to);
57+
const tag = tagName?.parent;
58+
const element = tag?.parent;
59+
60+
if (
61+
!tagName ||
62+
!tag ||
63+
!element ||
64+
element.name !== "Element" ||
65+
(tag.name !== "OpenTag" && tag.name !== "CloseTag")
66+
) {
67+
return null;
68+
}
69+
70+
const openTag = element.firstChild;
71+
const closeTag = element.lastChild;
72+
if (openTag?.name !== "OpenTag" || closeTag?.name !== "CloseTag") {
73+
return null;
74+
}
75+
76+
const openName = openTag.getChild("TagName");
77+
const closeName = closeTag.getChild("TagName");
78+
if (!openName || !closeName) return null;
79+
80+
const oldOpenName = readNode(state.doc, openName);
81+
const oldCloseName = readNode(state.doc, closeName);
82+
if (oldOpenName !== oldCloseName) return null;
83+
84+
return {
85+
tagName,
86+
pairedTagName: tag.name === "OpenTag" ? closeName : openName,
87+
};
88+
}
89+
90+
function getChangedRanges(transaction: Transaction): ChangedRange[] {
91+
const ranges: ChangedRange[] = [];
92+
transaction.changes.iterChanges((fromA, toA) => {
93+
ranges.push({ fromA, toA });
94+
});
95+
return ranges;
96+
}
97+
98+
function touchesRange(change: ChangedRange, from: number, to: number): boolean {
99+
if (change.fromA === change.toA) {
100+
return change.fromA >= from && change.fromA <= to;
101+
}
102+
return change.fromA < to && change.toA > from;
103+
}
104+
105+
function mapEditedRange(transaction: Transaction, from: number, to: number) {
106+
return {
107+
from: transaction.changes.mapPos(from, -1),
108+
to: transaction.changes.mapPos(to, 1),
109+
};
110+
}
111+
112+
function createPairedRename(transaction: Transaction): ChangeSpec | null {
113+
if (
114+
!transaction.docChanged ||
115+
transaction.annotation(skipTagAutoRename) ||
116+
transaction.annotation(Transaction.remote)
117+
) {
118+
return null;
119+
}
120+
121+
const ranges = getChangedRanges(transaction);
122+
if (ranges.length !== 1) return null;
123+
124+
const [change] = ranges;
125+
const target = getRenameTarget(
126+
transaction.startState,
127+
change.fromA,
128+
change.toA,
129+
);
130+
if (!target || !touchesRange(change, target.tagName.from, target.tagName.to)) {
131+
return null;
132+
}
133+
if (
134+
touchesRange(
135+
change,
136+
target.pairedTagName.from,
137+
target.pairedTagName.to,
138+
)
139+
) {
140+
return null;
141+
}
142+
143+
const editedRange = mapEditedRange(
144+
transaction,
145+
target.tagName.from,
146+
target.tagName.to,
147+
);
148+
const newName = transaction.newDoc.sliceString(
149+
editedRange.from,
150+
editedRange.to,
151+
);
152+
if (newName && !tagNamePattern.test(newName)) return null;
153+
154+
return {
155+
from: target.pairedTagName.from,
156+
to: target.pairedTagName.to,
157+
insert: newName,
158+
};
159+
}
160+
161+
export default function tagAutoRename(): Extension {
162+
return EditorState.transactionFilter.of((transaction) => {
163+
const pairedRename = createPairedRename(transaction);
164+
if (!pairedRename) return transaction;
165+
166+
return [
167+
transaction,
168+
{
169+
changes: pairedRename,
170+
annotations: [
171+
skipTagAutoRename.of(true),
172+
Transaction.userEvent.of("input.tag-rename"),
173+
],
174+
},
175+
];
176+
});
177+
}

src/lang/ar-ye.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -740,5 +740,7 @@
740740
"local word completion": "Local word completion",
741741
"settings-info-editor-local-word-completion": "Suggest words from the current file.",
742742
"terminal:failsafe": "FailSafe mode",
743-
"terminal:failsafe-info": "Start terminal with system shell"
743+
"terminal:failsafe-info": "Start terminal with system shell",
744+
"auto rename tags": "Auto rename tags",
745+
"settings-info-editor-auto-rename-tags": "Rename the matching opening or closing tag while editing HTML-like tags."
744746
}

src/lang/be-by.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -740,5 +740,7 @@
740740
"local word completion": "Local word completion",
741741
"settings-info-editor-local-word-completion": "Suggest words from the current file.",
742742
"terminal:failsafe": "FailSafe mode",
743-
"terminal:failsafe-info": "Start terminal with system shell"
743+
"terminal:failsafe-info": "Start terminal with system shell",
744+
"auto rename tags": "Auto rename tags",
745+
"settings-info-editor-auto-rename-tags": "Rename the matching opening or closing tag while editing HTML-like tags."
744746
}

src/lang/bn-bd.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -740,5 +740,7 @@
740740
"local word completion": "Local word completion",
741741
"settings-info-editor-local-word-completion": "Suggest words from the current file.",
742742
"terminal:failsafe": "FailSafe mode",
743-
"terminal:failsafe-info": "Start terminal with system shell"
743+
"terminal:failsafe-info": "Start terminal with system shell",
744+
"auto rename tags": "Auto rename tags",
745+
"settings-info-editor-auto-rename-tags": "Rename the matching opening or closing tag while editing HTML-like tags."
744746
}

src/lang/cs-cz.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -740,5 +740,7 @@
740740
"local word completion": "Local word completion",
741741
"settings-info-editor-local-word-completion": "Suggest words from the current file.",
742742
"terminal:failsafe": "FailSafe mode",
743-
"terminal:failsafe-info": "Start terminal with system shell"
743+
"terminal:failsafe-info": "Start terminal with system shell",
744+
"auto rename tags": "Auto rename tags",
745+
"settings-info-editor-auto-rename-tags": "Rename the matching opening or closing tag while editing HTML-like tags."
744746
}

src/lang/de-de.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -740,5 +740,7 @@
740740
"local word completion": "Local word completion",
741741
"settings-info-editor-local-word-completion": "Suggest words from the current file.",
742742
"terminal:failsafe": "FailSafe mode",
743-
"terminal:failsafe-info": "Start terminal with system shell"
743+
"terminal:failsafe-info": "Start terminal with system shell",
744+
"auto rename tags": "Auto rename tags",
745+
"settings-info-editor-auto-rename-tags": "Rename the matching opening or closing tag while editing HTML-like tags."
744746
}

src/lang/en-us.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@
134134
"live autocompletion": "Live autocompletion",
135135
"local word completion": "Local word completion",
136136
"auto close tags": "Auto close tags",
137+
"auto rename tags": "Auto rename tags",
137138
"file properties": "File properties",
138139
"path": "Path",
139140
"type": "Type",
@@ -671,6 +672,7 @@
671672
"settings-info-editor-live-autocomplete": "Show suggestions while you type.",
672673
"settings-info-editor-local-word-completion": "Suggest words from the current file.",
673674
"settings-info-editor-auto-close-tags": "Automatically insert closing tags in HTML, XML, Vue, Angular, and PHP template files.",
675+
"settings-info-editor-auto-rename-tags": "Rename the matching opening or closing tag while editing HTML-like tags.",
674676
"settings-info-editor-rainbow-brackets": "Color matching brackets by nesting depth.",
675677
"settings-info-editor-relative-line-numbers": "Show distance from the current line.",
676678
"settings-info-editor-rtl-text": "Switch right-to-left behavior per line.",

src/lang/es-sv.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -740,5 +740,7 @@
740740
"local word completion": "Local word completion",
741741
"settings-info-editor-local-word-completion": "Suggest words from the current file.",
742742
"terminal:failsafe": "FailSafe mode",
743-
"terminal:failsafe-info": "Start terminal with system shell"
743+
"terminal:failsafe-info": "Start terminal with system shell",
744+
"auto rename tags": "Auto rename tags",
745+
"settings-info-editor-auto-rename-tags": "Rename the matching opening or closing tag while editing HTML-like tags."
744746
}

src/lang/fr-fr.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -740,5 +740,7 @@
740740
"local word completion": "Local word completion",
741741
"settings-info-editor-local-word-completion": "Suggest words from the current file.",
742742
"terminal:failsafe": "FailSafe mode",
743-
"terminal:failsafe-info": "Start terminal with system shell"
743+
"terminal:failsafe-info": "Start terminal with system shell",
744+
"auto rename tags": "Auto rename tags",
745+
"settings-info-editor-auto-rename-tags": "Rename the matching opening or closing tag while editing HTML-like tags."
744746
}

src/lang/he-il.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -740,5 +740,7 @@
740740
"local word completion": "Local word completion",
741741
"settings-info-editor-local-word-completion": "Suggest words from the current file.",
742742
"terminal:failsafe": "FailSafe mode",
743-
"terminal:failsafe-info": "Start terminal with system shell"
743+
"terminal:failsafe-info": "Start terminal with system shell",
744+
"auto rename tags": "Auto rename tags",
745+
"settings-info-editor-auto-rename-tags": "Rename the matching opening or closing tag while editing HTML-like tags."
744746
}

0 commit comments

Comments
 (0)