[astro] WEB-76003 Improve attribute insert handling for Astro

Space-RevId: 89d349e837e10f77861ac1b0fd3534dfab7474cb

GitOrigin-RevId: e61ff48a5fc55575dd3acdeda14ccbfc2e852374
This commit is contained in:
Denis Bezrukov
2026-01-27 14:32:54 +00:00
committed by intellij-monorepo-bot
parent 38c3ee874d
commit 6201848e37
3 changed files with 133 additions and 3 deletions
@@ -0,0 +1,81 @@
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInsight.completion
import com.intellij.application.options.editor.WebEditorOptions
import com.intellij.codeInsight.AutoPopupController
import com.intellij.codeInsight.editorActions.TabOutScopesTracker
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.openapi.editor.ScrollType
import com.intellij.util.text.CharArrayUtil
class XmlAttributeValueQuotationHandler(private val myStyle: AttributeValueQuotationStyle) : InsertHandler<LookupElement> {
enum class AttributeValueQuotationStyle { QUOTES, BRACES, EQUAL_ONLY }
companion object {
@JvmField
val QUOTES: XmlAttributeValueQuotationHandler = XmlAttributeValueQuotationHandler(AttributeValueQuotationStyle.QUOTES)
@JvmField
val BRACES: XmlAttributeValueQuotationHandler = XmlAttributeValueQuotationHandler(AttributeValueQuotationStyle.BRACES)
@JvmField
val EQUAL_ONLY: XmlAttributeValueQuotationHandler = XmlAttributeValueQuotationHandler(AttributeValueQuotationStyle.EQUAL_ONLY)
}
override fun handleInsert(context: InsertionContext, item: LookupElement) {
val editor = context.editor
val document = editor.document
val caretOffset = editor.caretModel.offset
val chars = document.charsSequence
val hasQuotes = CharArrayUtil.regionMatches(chars, caretOffset, "=\"") ||
CharArrayUtil.regionMatches(chars, caretOffset, "='")
val hasBraces = CharArrayUtil.regionMatches(chars, caretOffset, "={")
val hasValue = hasQuotes || hasBraces
if (!hasValue && CharArrayUtil.regionMatches(chars, caretOffset, "=")) {
document.deleteString(caretOffset, caretOffset + 1)
}
var effective = myStyle
val insertQuotes = WebEditorOptions.getInstance().isInsertQuotesForAttributeValue
if (!insertQuotes && effective == AttributeValueQuotationStyle.QUOTES) {
effective = AttributeValueQuotationStyle.EQUAL_ONLY
}
if (!hasValue) {
val toInsert = when (effective) {
AttributeValueQuotationStyle.BRACES -> "={}"
AttributeValueQuotationStyle.QUOTES -> "=\"\""
AttributeValueQuotationStyle.EQUAL_ONLY -> "="
}
if (caretOffset < document.textLength && "/> \n\t\r".indexOf(chars[caretOffset]) < 0) {
document.insertString(caretOffset, "$toInsert ")
}
else {
document.insertString(caretOffset, toInsert)
}
if ('=' == context.completionChar) {
context.setAddCompletionChar(false)
}
}
val move = when (effective) {
AttributeValueQuotationStyle.BRACES, AttributeValueQuotationStyle.QUOTES -> 2
AttributeValueQuotationStyle.EQUAL_ONLY -> 1
}
editor.caretModel.moveToOffset(caretOffset + move)
if (effective != AttributeValueQuotationStyle.EQUAL_ONLY || hasValue) {
TabOutScopesTracker.getInstance().registerEmptyScopeAtCaret(editor)
}
editor.scrollingModel.scrollToCaret(ScrollType.RELATIVE)
editor.selectionModel.removeSelection()
if (effective == AttributeValueQuotationStyle.BRACES) {
AutoPopupController.getInstance(context.project).autoPopupParameterInfo(editor, null)
} else {
AutoPopupController.getInstance(context.project).scheduleAutoPopup(editor)
}
}
}
@@ -20,7 +20,11 @@ import com.intellij.psi.PsiElement
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.xml.XmlAttribute
class HtmlAttributeSymbolsCompletionProvider : PolySymbolsCompletionProviderBase<XmlAttribute>() {
import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.polySymbols.completion.PolySymbolCodeCompletionItem
open class HtmlAttributeSymbolsCompletionProvider : PolySymbolsCompletionProviderBase<XmlAttribute>() {
override fun getContext(position: PsiElement): XmlAttribute? =
PsiTreeUtil.getParentOfType(position, XmlAttribute::class.java)
@@ -81,8 +85,9 @@ class HtmlAttributeSymbolsCompletionProvider : PolySymbolsCompletionProviderBase
.asSingleSymbol() ?: return@runWithTimeoutOrNull null
HtmlAttributeSymbolInfo.create(fullName, freshRegistry, match, insertionContext.file)
}
if (info != null && info.acceptsValue && !info.acceptsNoValue) {
XmlAttributeInsertHandler.INSTANCE.handleInsert(insertionContext, lookupItem)
if (info != null && shouldInsertValue(parameters, item, info)) {
val handler = selectInsertHandler(parameters, item, info)
handler.handleInsert(insertionContext, lookupItem)
}
}
).addToResult(parameters, patchedResultSet)
@@ -102,4 +107,22 @@ class HtmlAttributeSymbolsCompletionProvider : PolySymbolsCompletionProviderBase
}
}
protected open fun selectInsertHandler(
parameters: CompletionParameters,
item: PolySymbolCodeCompletionItem,
info: HtmlAttributeSymbolInfo
): InsertHandler<LookupElement> {
val framework = HtmlFrameworkSymbolsSupport.get(info.symbol)
return framework.createAttributeInsertHandler(parameters, item, info) ?: XmlAttributeInsertHandler.INSTANCE
}
protected open fun shouldInsertValue(
parameters: CompletionParameters,
item: PolySymbolCodeCompletionItem,
info: HtmlAttributeSymbolInfo
): Boolean {
val framework = HtmlFrameworkSymbolsSupport.get(info.symbol)
return framework.shouldInsertAttributeValue(parameters, item, info)
}
}
@@ -1,8 +1,12 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.polySymbols.html
import com.intellij.codeInsight.completion.CompletionParameters
import com.intellij.codeInsight.completion.InsertHandler
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.polySymbols.PolySymbol
import com.intellij.polySymbols.PolySymbolProperty
import com.intellij.polySymbols.completion.PolySymbolCodeCompletionItem
import com.intellij.polySymbols.framework.FrameworkId
import com.intellij.polySymbols.framework.PolySymbolFramework
import com.intellij.polySymbols.html.attributes.HtmlAttributeSymbolDescriptor
@@ -22,6 +26,28 @@ interface HtmlFrameworkSymbolsSupport {
fun getAttributeNameCodeCompletionFilter(tag: XmlTag): Predicate<String> = Predicate { true }
/**
* Creates an insert handler for the given attribute completion item.
* Override this method to provide framework-specific insert handlers (e.g., braces for JSX-like syntax).
*
* Default implementation returns null, which will use the standard XML attribute insert handler.
*/
fun createAttributeInsertHandler(
parameters: CompletionParameters,
item: PolySymbolCodeCompletionItem,
info: HtmlAttributeSymbolInfo
): InsertHandler<LookupElement>? = null
/**
* Determines whether to insert a value (quotes/braces) after the attribute name.
* Override this method to customize when values should be inserted (e.g., always insert for directives).
*/
fun shouldInsertAttributeValue(
parameters: CompletionParameters,
item: PolySymbolCodeCompletionItem,
info: HtmlAttributeSymbolInfo
): Boolean = info.acceptsValue && !info.acceptsNoValue
companion object {
/**
* Provides id of the Symbol's framework, e.g. vue, angular, react, etc.