Skip to content
This repository was archived by the owner on Apr 15, 2026. It is now read-only.

Commit b84ed85

Browse files
committed
Split auto tag closing into two transactions
FIX: `autoCloseTags` now generates two separate transactions, so that the completion can be undone separately. Closes codemirror/dev#1235
1 parent 2ee7df7 commit b84ed85

2 files changed

Lines changed: 22 additions & 23 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"@codemirror/language": "^6.6.0",
3131
"@codemirror/lint": "^6.0.0",
3232
"@codemirror/state": "^6.0.0",
33-
"@codemirror/view": "^6.0.0",
33+
"@codemirror/view": "^6.17.0",
3434
"@lezer/common": "^1.0.0",
3535
"@lezer/javascript": "^1.0.0"
3636
},

src/javascript.ts

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -113,44 +113,43 @@ function elementName(doc: Text, tree: SyntaxNode | null | undefined, max = doc.l
113113
return ""
114114
}
115115

116-
function isEndTag(node: SyntaxNode | null) {
117-
return node && (node.name == "JSXEndTag" || node.name == "JSXSelfCloseEndTag")
118-
}
119-
120116
const android = typeof navigator == "object" && /Android\b/.test(navigator.userAgent)
121117

122118
/// Extension that will automatically insert JSX close tags when a `>` or
123119
/// `/` is typed.
124-
export const autoCloseTags = EditorView.inputHandler.of((view, from, to, text) => {
120+
export const autoCloseTags = EditorView.inputHandler.of((view, from, to, text, defaultInsert) => {
125121
if ((android ? view.composing : view.compositionStarted) || view.state.readOnly ||
126122
from != to || (text != ">" && text != "/") ||
127123
!javascriptLanguage.isActiveAt(view.state, from, -1)) return false
128-
let {state} = view
129-
let changes = state.changeByRange(range => {
130-
let {head} = range, around = syntaxTree(state).resolveInner(head, -1), name
124+
let base = defaultInsert(), {state} = base
125+
let closeTags = state.changeByRange(range => {
126+
let {head} = range, around = syntaxTree(state).resolveInner(head - 1, -1), name
131127
if (around.name == "JSXStartTag") around = around.parent!
132-
if (around.name == "JSXAttributeValue" && around.to > head) {
133-
// Ignore input inside attribute
128+
if (state.doc.sliceString(head - 1, head) != text || around.name == "JSXAttributeValue" && around.to > head) {
129+
// Ignore input inside attribute or cases where the text wasn't actually inserted
134130
} else if (text == ">" && around.name == "JSXFragmentTag") {
135-
return {range: EditorSelection.cursor(head + 1), changes: {from: head, insert: `></>`}}
136-
} else if (text == "/" && around.name == "JSXFragmentTag") {
137-
let empty = around.parent, base = empty?.parent
138-
if (empty!.from == head - 1 && base!.lastChild?.name != "JSXEndTag" &&
139-
(name = elementName(state.doc, base?.firstChild, head))) {
140-
let insert = `/${name}>`
141-
return {range: EditorSelection.cursor(head + insert.length), changes: {from: head, insert}}
131+
return {range, changes: {from: head, insert: `</>`}}
132+
} else if (text == "/" && around.name == "JSXStartCloseTag") {
133+
let empty = around.parent!, base = empty.parent
134+
if (base && empty.from == head - 2 &&
135+
((name = elementName(state.doc, base.firstChild, head)) || base.firstChild?.name == "JSXFragmentTag")) {
136+
let insert = `${name}>`
137+
return {range: EditorSelection.cursor(head + insert.length, -1), changes: {from: head, insert}}
142138
}
143139
} else if (text == ">") {
144140
let openTag = findOpenTag(around)
145-
if (openTag && !isEndTag(openTag.lastChild) &&
146-
state.sliceDoc(head, head + 2) != "</" &&
141+
if (openTag &&
142+
!/^\/?>|^<\//.test(state.doc.sliceString(head, head + 2)) &&
147143
(name = elementName(state.doc, openTag, head)))
148-
return {range: EditorSelection.cursor(head + 1), changes: {from: head, insert: `></${name}>`}}
144+
return {range, changes: {from: head, insert: `</${name}>`}}
149145
}
150146
return {range}
151147
})
152-
if (changes.changes.empty) return false
153-
view.dispatch(changes, {userEvent: "input.type", scrollIntoView: true})
148+
if (closeTags.changes.empty) return false
149+
view.dispatch([
150+
base,
151+
state.update(closeTags, {userEvent: "input.complete", scrollIntoView: true})
152+
])
154153
return true
155154
});
156155

0 commit comments

Comments
 (0)