diff --git a/platform/lang-api/src/com/intellij/psi/codeStyle/CodeStyleSettings.java b/platform/lang-api/src/com/intellij/psi/codeStyle/CodeStyleSettings.java index fd7fe543f17c..04797764bc7e 100644 --- a/platform/lang-api/src/com/intellij/psi/codeStyle/CodeStyleSettings.java +++ b/platform/lang-api/src/com/intellij/psi/codeStyle/CodeStyleSettings.java @@ -403,6 +403,7 @@ public class CodeStyleSettings extends CommonCodeStyleSettings implements Clonea "a,abbr,acronym,b,basefont,bdo,big,br,cite,cite,code,dfn,em,font,i,img,input,kbd,label,q,s,samp,select,span,strike,strong,sub,sup,textarea,tt,u,var"; @NonNls public String HTML_DONT_ADD_BREAKS_IF_INLINE_CONTENT = "title,h1,h2,h3,h4,h5,h6,p"; public QuoteStyle HTML_QUOTE_STYLE = QuoteStyle.Double; + public boolean HTML_ENFORCE_QUOTES = false; // --------------------------------------------------------------------------------------- diff --git a/platform/platform-resources-en/src/messages/ApplicationBundle.properties b/platform/platform-resources-en/src/messages/ApplicationBundle.properties index 4a8c3bad5ad4..98684cec09d7 100644 --- a/platform/platform-resources-en/src/messages/ApplicationBundle.properties +++ b/platform/platform-resources-en/src/messages/ApplicationBundle.properties @@ -90,6 +90,7 @@ checkbox.parentheses.around.method.arguments=Add parentheses around method argum checkbox.rename.local.variables.inplace=Enable in-place mode checkbox.rename.local.variables.preselect=Preselect old name generated.quote.marks=Generated quote marks: +generated.quote.enforce.format=Enforce on format editbox.keep.blank.lines=Keep blank lines: checkbox.keep.white.spaces=Keep white spaces checkbox.align.text=Align text diff --git a/platform/platform-resources/src/META-INF/XmlPlugin.xml b/platform/platform-resources/src/META-INF/XmlPlugin.xml index 1a244d0e4dcc..8e1583f9a1cb 100644 --- a/platform/platform-resources/src/META-INF/XmlPlugin.xml +++ b/platform/platform-resources/src/META-INF/XmlPlugin.xml @@ -234,6 +234,7 @@ + diff --git a/platform/platform-resources/src/codeStyle/preview/preview.html.template b/platform/platform-resources/src/codeStyle/preview/preview.html.template index a716ad0366f5..bed8287f60e6 100644 --- a/platform/platform-resources/src/codeStyle/preview/preview.html.template +++ b/platform/platform-resources/src/codeStyle/preview/preview.html.template @@ -12,12 +12,12 @@ -
+
-
- +
+ - diff --git a/xml/impl/src/com/intellij/application/options/CodeStyleHtmlPanel.form b/xml/impl/src/com/intellij/application/options/CodeStyleHtmlPanel.form index 8c53c6bd7664..e281cbe66218 100644 --- a/xml/impl/src/com/intellij/application/options/CodeStyleHtmlPanel.form +++ b/xml/impl/src/com/intellij/application/options/CodeStyleHtmlPanel.form @@ -3,7 +3,7 @@ - + @@ -40,7 +40,7 @@ - + @@ -194,6 +194,14 @@ + + + + + + + + diff --git a/xml/impl/src/com/intellij/application/options/CodeStyleHtmlPanel.java b/xml/impl/src/com/intellij/application/options/CodeStyleHtmlPanel.java index 3b971b9cd921..63d4c96a1cd8 100644 --- a/xml/impl/src/com/intellij/application/options/CodeStyleHtmlPanel.java +++ b/xml/impl/src/com/intellij/application/options/CodeStyleHtmlPanel.java @@ -29,6 +29,7 @@ import com.intellij.psi.PsiFile; import com.intellij.psi.codeStyle.CodeStyleSettings; import com.intellij.psi.codeStyle.CommonCodeStyleSettings; import com.intellij.ui.EnumComboBoxModel; +import com.intellij.ui.components.JBCheckBox; import com.intellij.ui.components.JBScrollPane; import com.intellij.util.ArrayUtil; import com.intellij.util.PlatformIcons; @@ -68,6 +69,7 @@ public class CodeStyleHtmlPanel extends CodeStyleAbstractPanel { private JBScrollPane myJBScrollPane; private JPanel myRightMarginPanel; private JComboBox myQuotesCombo; + private JBCheckBox myEnforceQuotesBox; private RightMarginForm myRightMarginForm; public CodeStyleHtmlPanel(CodeStyleSettings settings) { @@ -91,7 +93,14 @@ public class CodeStyleHtmlPanel extends CodeStyleAbstractPanel { myInlineElementsTagNames.getTextField().setColumns(5); myDontBreakIfInlineContent.getTextField().setColumns(5); - + myQuotesCombo.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + boolean quotesRequired = !CodeStyleSettings.QuoteStyle.None.equals(myQuotesCombo.getSelectedItem()); + myEnforceQuotesBox.setEnabled(quotesRequired); + if (!quotesRequired) myEnforceQuotesBox.setSelected(false); + } + }); addPanelToWatch(myPanel); } @@ -166,6 +175,7 @@ public class CodeStyleHtmlPanel extends CodeStyleAbstractPanel { settings.HTML_KEEP_LINE_BREAKS = myShouldKeepBlankLines.isSelected(); settings.HTML_KEEP_LINE_BREAKS_IN_TEXT = myShouldKeepLineBreaksInText.isSelected(); settings.HTML_QUOTE_STYLE = (CodeStyleSettings.QuoteStyle)myQuotesCombo.getSelectedItem(); + settings.HTML_ENFORCE_QUOTES = myEnforceQuotesBox.isSelected(); myRightMarginForm.apply(settings); } @@ -206,6 +216,7 @@ public class CodeStyleHtmlPanel extends CodeStyleAbstractPanel { myKeepWhiteSpacesTagNames.setText(settings.HTML_KEEP_WHITESPACES_INSIDE); myRightMarginForm.reset(settings); myQuotesCombo.setSelectedItem(settings.HTML_QUOTE_STYLE); + myEnforceQuotesBox.setSelected(settings.HTML_ENFORCE_QUOTES); } @Override @@ -280,7 +291,8 @@ public class CodeStyleHtmlPanel extends CodeStyleAbstractPanel { return true; } - return myRightMarginForm.isModified(settings); + return myRightMarginForm.isModified(settings) || + myEnforceQuotesBox.isSelected() != settings.HTML_ENFORCE_QUOTES; } @Override diff --git a/xml/impl/src/com/intellij/lang/html/HtmlQuotesFormatPreprocessor.java b/xml/impl/src/com/intellij/lang/html/HtmlQuotesFormatPreprocessor.java new file mode 100644 index 000000000000..d2553e0b4cb5 --- /dev/null +++ b/xml/impl/src/com/intellij/lang/html/HtmlQuotesFormatPreprocessor.java @@ -0,0 +1,172 @@ +/* + * Copyright 2000-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.lang.html; + + +import com.intellij.lang.ASTNode; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.XmlRecursiveElementVisitor; +import com.intellij.psi.codeStyle.CodeStyleSettings; +import com.intellij.psi.codeStyle.CodeStyleSettingsManager; +import com.intellij.psi.impl.source.codeStyle.PreFormatProcessor; +import com.intellij.psi.tree.IElementType; +import com.intellij.psi.xml.XmlAttributeValue; +import com.intellij.psi.xml.XmlTokenType; +import com.intellij.util.DocumentUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class HtmlQuotesFormatPreprocessor implements PreFormatProcessor { + @NotNull + @Override + public TextRange process(@NotNull ASTNode node, @NotNull TextRange range) { + PsiElement psiElement = node.getPsi(); + if (psiElement != null && + psiElement.isValid() && + psiElement.getLanguage().is(HTMLLanguage.INSTANCE)) { + CodeStyleSettings settings = CodeStyleSettingsManager.getSettings(psiElement.getProject()); + CodeStyleSettings.QuoteStyle quoteStyle = settings.HTML_QUOTE_STYLE; + if (quoteStyle != CodeStyleSettings.QuoteStyle.None && settings.HTML_ENFORCE_QUOTES) { + HtmlQuotesConverter converter = new HtmlQuotesConverter(quoteStyle, psiElement, range); + Document document = converter.getDocument(); + if (document != null) { + DocumentUtil.executeInBulk(document, true, converter); + } + return converter.getTextRange(); + } + } + return range; + } + + + private static class HtmlQuotesConverter extends XmlRecursiveElementVisitor implements Runnable { + private TextRange myTextRange; + private final Document myDocument; + private final PsiDocumentManager myDocumentManager; + private final PsiElement myElement; + private int myDelta = 0; + private final char myQuoteChar; + + private HtmlQuotesConverter(CodeStyleSettings.QuoteStyle style, + @NotNull PsiElement element, + @NotNull TextRange textRange) { + Project project = element.getProject(); + PsiFile file = element.getContainingFile(); + myElement = element; + myTextRange = new TextRange(textRange.getStartOffset(), textRange.getEndOffset()); + myDocumentManager = PsiDocumentManager.getInstance(project); + myDocument = myDocumentManager.getDocument(file); + switch (style) { + case Single: + myQuoteChar = '\''; + break; + case Double: + myQuoteChar = '"'; + break; + default: + myQuoteChar = 0; + } + } + + public TextRange getTextRange() { + return myTextRange.grown(myDelta); + } + + public Document getDocument() { + return myDocument; + } + + @Override + public void visitXmlAttributeValue(XmlAttributeValue value) { + if (myTextRange.contains(value.getTextRange())) { + PsiElement child = value.getFirstChild(); + if (child != null && + !containsQuoteChars(value) // For now we skip values containing quotes to be inserted/replaced + ) { + String newValue = null; + if (child.getNode().getElementType() == XmlTokenType.XML_ATTRIBUTE_VALUE_START_DELIMITER) { + PsiElement lastChild = value.getLastChild(); + if (lastChild != null && lastChild.getNode().getElementType() == XmlTokenType.XML_ATTRIBUTE_VALUE_END_DELIMITER) { + CharSequence delimiterChars = child.getNode().getChars(); + if (delimiterChars.length() == 1) { + char existingQuote = delimiterChars.charAt(0); + if (existingQuote != myQuoteChar) { + newValue = convertQuotes(value); + } + } + } + } + else if (child.getNode().getElementType() == XmlTokenType.XML_ATTRIBUTE_VALUE_TOKEN + && child == value.getLastChild()) { + newValue = surroundWithQuotes(value); + } + if (newValue != null) { + int startOffset = value.getTextRange().getStartOffset() + myDelta; + int endOffset = value.getTextRange().getEndOffset() + myDelta; + myDocument.replaceString(startOffset, endOffset, newValue); + myDelta += newValue.length() - value.getTextLength(); + } + } + } + } + + @Nullable + private String convertQuotes(@NotNull XmlAttributeValue value) { + String currValue = value.getNode().getChars().toString(); + if (currValue.length() >= 2) { + return myQuoteChar + currValue.substring(1, currValue.length() - 1) + myQuoteChar; + } + return null; + } + + @NotNull + private String surroundWithQuotes(@NotNull XmlAttributeValue value) { + String currValue = value.getNode().getChars().toString(); + return myQuoteChar + currValue + myQuoteChar; + } + + private boolean containsQuoteChars(@NotNull XmlAttributeValue value) { + for (PsiElement child = value.getFirstChild(); child != null; child = child.getNextSibling()) { + if (!isDelimiter(child.getNode().getElementType())) { + CharSequence valueChars = child.getNode().getChars(); + for (int i = 0; i < valueChars.length(); i ++) { + if (valueChars.charAt(i) == myQuoteChar) return true; + } + } + } + return false; + } + + private static boolean isDelimiter(@NotNull IElementType elementType) { + return elementType == XmlTokenType.XML_ATTRIBUTE_VALUE_START_DELIMITER || + elementType == XmlTokenType.XML_ATTRIBUTE_VALUE_END_DELIMITER; + } + + @Override + public void run() { + if (myDocument != null) { + myDocumentManager.doPostponedOperationsAndUnblockDocument(myDocument); + myElement.accept(this); + myDocumentManager.commitDocument(myDocument); + } + } + } +}