From c2c1b7f9a4affcf8eca974f5365a75ff2c8642bd Mon Sep 17 00:00:00 2001 From: Nishthajain7 Date: Sun, 22 Feb 2026 21:17:27 +0530 Subject: [PATCH 1/2] fix: Rename both tags when one is being edited --- .../IDE/components/Editor/stateUtils.js | 58 ++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/client/modules/IDE/components/Editor/stateUtils.js b/client/modules/IDE/components/Editor/stateUtils.js index fd45cce098..9a721d035f 100644 --- a/client/modules/IDE/components/Editor/stateUtils.js +++ b/client/modules/IDE/components/Editor/stateUtils.js @@ -44,7 +44,7 @@ import { } from '@emmetio/codemirror6-plugin'; import { css } from '@codemirror/lang-css'; -import { html } from '@codemirror/lang-html'; +import { html, autoCloseTags } from '@codemirror/lang-html'; import { json } from '@codemirror/lang-json'; import { xml } from '@codemirror/lang-xml'; import { linter } from '@codemirror/lint'; @@ -52,6 +52,8 @@ import { JSHINT } from 'jshint'; import { HTMLHint } from 'htmlhint'; import { CSSLint } from 'csslint'; import { emmetConfig } from '@emmetio/codemirror6-plugin'; +import { syntaxTree } from '@codemirror/language'; +import { Annotation } from '@codemirror/state'; import p5JavaScript from './p5JavaScript'; import tidyCodeWithPrettier from './tidier'; @@ -269,6 +271,56 @@ export const AUTOCOMPLETE_OPTIONS = { closeOnBlur: false }; +const tagSyncAnnotation = Annotation.define(); +const tagSyncExtension = EditorView.updateListener.of((update) => { + if (!update.docChanged) return; + if (update.transactions.some((tr) => tr.annotation(tagSyncAnnotation))) + return; + + update.transactions.forEach((tr) => { + if (!tr.isUserEvent('input') && !tr.isUserEvent('delete')) return; + + const tree = syntaxTree(update.state); + const { doc } = update.state; + + tr.changes.iterChanges((_fromA, _toA, fromB, _toB) => { + const node = tree.resolveInner(fromB, -1); + if (node.type.name !== 'TagName') return; + + const openTag = node.parent; + if (!openTag || openTag.type.name !== 'OpenTag') return; + + const elementNode = openTag.parent; + if (!elementNode || elementNode.type.name !== 'Element') return; + + const closeTag = + elementNode.getChild('EndTag') || + elementNode.getChild('MismatchedCloseTag'); + if (!closeTag) return; + + const closeTagName = closeTag.getChild('TagName'); + if (!closeTagName) return; + + const newTagName = doc.sliceString(node.from, node.to); + const currentCloseName = doc.sliceString( + closeTagName.from, + closeTagName.to + ); + + if (!newTagName || currentCloseName === newTagName) return; + + update.view.dispatch({ + changes: { + from: closeTagName.from, + to: closeTagName.to, + insert: newTagName + }, + annotations: tagSyncAnnotation.of(true) + }); + }); + }); +}); + /** * Creates a new CodeMirror editor state with configurations, * extensions, and keymaps tailored to the file type and settings. @@ -359,6 +411,10 @@ export function createNewFileState(filename, document, settings) { if (fileEmmetConfig) { extensions.push(fileEmmetConfig); extensions.push(abbreviationTracker()); + if (getFileMode(filename) === 'html') { + extensions.push(autoCloseTags); + extensions.push(tagSyncExtension); + } keymaps.push(emmetKeymaps); } From fef46c3a0a048572e9d270d39b44638e22d7dbe2 Mon Sep 17 00:00:00 2001 From: Nishthajain7 Date: Mon, 23 Feb 2026 22:42:20 +0530 Subject: [PATCH 2/2] fix: improve html tag renaming --- .../IDE/components/Editor/stateUtils.js | 87 ++++++++++++++----- client/styles/components/_asset-list.scss | 1 + 2 files changed, 68 insertions(+), 20 deletions(-) diff --git a/client/modules/IDE/components/Editor/stateUtils.js b/client/modules/IDE/components/Editor/stateUtils.js index 9a721d035f..09ec322add 100644 --- a/client/modules/IDE/components/Editor/stateUtils.js +++ b/client/modules/IDE/components/Editor/stateUtils.js @@ -1,4 +1,4 @@ -import { EditorState, Compartment } from '@codemirror/state'; +import { EditorState, Compartment, Annotation } from '@codemirror/state'; import { EditorView, lineNumbers as lineNumbersExt, @@ -17,7 +17,8 @@ import { foldKeymap, bracketMatching, indentOnInput, - syntaxHighlighting + syntaxHighlighting, + syntaxTree } from '@codemirror/language'; import { autocompletion, @@ -52,8 +53,6 @@ import { JSHINT } from 'jshint'; import { HTMLHint } from 'htmlhint'; import { CSSLint } from 'csslint'; import { emmetConfig } from '@emmetio/codemirror6-plugin'; -import { syntaxTree } from '@codemirror/language'; -import { Annotation } from '@codemirror/state'; import p5JavaScript from './p5JavaScript'; import tidyCodeWithPrettier from './tidier'; @@ -293,28 +292,76 @@ const tagSyncExtension = EditorView.updateListener.of((update) => { const elementNode = openTag.parent; if (!elementNode || elementNode.type.name !== 'Element') return; - const closeTag = - elementNode.getChild('EndTag') || - elementNode.getChild('MismatchedCloseTag'); - if (!closeTag) return; + const newTagName = doc.sliceString(node.from, node.to); + if (!newTagName) return; - const closeTagName = closeTag.getChild('TagName'); - if (!closeTagName) return; + const oldPos = tr.isUserEvent('delete') + ? tr.changes.mapPos(fromB, 1) + : tr.changes.mapPos(fromB, -1); - const newTagName = doc.sliceString(node.from, node.to); - const currentCloseName = doc.sliceString( - closeTagName.from, - closeTagName.to + const oldTree = syntaxTree(update.startState); + const oldNode = oldTree.resolveInner(oldPos, -1); + if (oldNode.type.name !== 'TagName') return; + + const originalTagName = update.startState.doc.sliceString( + oldNode.from, + oldNode.to + ); + if (!originalTagName || originalTagName === newTagName) return; + + const oldOpenTag = oldNode.parent; + if (!oldOpenTag || oldOpenTag.type.name !== 'OpenTag') return; + + const oldElementNode = oldOpenTag.parent; + if (!oldElementNode || oldElementNode.type.name !== 'Element') return; + + const oldParentElement = oldElementNode.parent; + + let oldCloseTag = null; + let child = oldElementNode.firstChild; + while (child) { + if (child.type.name === 'EndTag' || child.type.name === 'CloseTag') { + oldCloseTag = child; + break; + } + child = child.nextSibling; + } + if (!oldCloseTag) return; + + let parentHasCloseTag = false; + let parentChild = oldParentElement?.firstChild; + while (parentChild) { + if ( + parentChild.type.name === 'EndTag' || + parentChild.type.name === 'CloseTag' + ) { + parentHasCloseTag = true; + break; + } + parentChild = parentChild.nextSibling; + } + if (!parentHasCloseTag) return; + + const textBetween = update.startState.doc.sliceString( + oldOpenTag.to, + oldCloseTag.from ); + const opensInBetween = ( + textBetween.match(new RegExp(`<${originalTagName}[\\s>]`, 'gi')) || [] + ).length; + const closesInBetween = ( + textBetween.match(new RegExp(`<\\/${originalTagName}\\s*>`, 'gi')) || [] + ).length; + if (opensInBetween !== closesInBetween) return; + + const oldCloseTagName = oldCloseTag.getChild('TagName'); + if (!oldCloseTagName) return; - if (!newTagName || currentCloseName === newTagName) return; + const newCloseStart = tr.changes.mapPos(oldCloseTagName.from); + const newCloseEnd = tr.changes.mapPos(oldCloseTagName.to); update.view.dispatch({ - changes: { - from: closeTagName.from, - to: closeTagName.to, - insert: newTagName - }, + changes: { from: newCloseStart, to: newCloseEnd, insert: newTagName }, annotations: tagSyncAnnotation.of(true) }); }); diff --git a/client/styles/components/_asset-list.scss b/client/styles/components/_asset-list.scss index f359c1ae3b..9fcd06f4e1 100644 --- a/client/styles/components/_asset-list.scss +++ b/client/styles/components/_asset-list.scss @@ -35,6 +35,7 @@ margin: #{math.div(10, $base-font-size)}rem; height: #{math.div(72, $base-font-size)}rem; font-size: #{math.div(16, $base-font-size)}rem; + text-decoration: underline; } .asset-table__row:nth-child(odd) {