diff --git a/ide/css.editor/nbproject/project.properties b/ide/css.editor/nbproject/project.properties index ca4bfda6513b..91a7d0213ae0 100644 --- a/ide/css.editor/nbproject/project.properties +++ b/ide/css.editor/nbproject/project.properties @@ -15,6 +15,8 @@ # specific language governing permissions and limitations # under the License. +auxiliary.org-netbeans-modules-css-prep.less_2e_configured=true +auxiliary.org-netbeans-modules-css-prep.sass_2e_configured=true release.external/css21-spec.zip=docs/css21-spec.zip release.external/css3-spec.zip=docs/css3-spec.zip @@ -33,4 +35,4 @@ test.config.stableBTD.excludes=\ **/properties/parser/PropertyValueTest.class,\ **/CssBracketCompleterTest.class -test-unit-sys-prop.netbeans.dirs=${netbeans.dest.dir}/${nb.cluster.ide.dir} \ No newline at end of file +test-unit-sys-prop.netbeans.dirs=${netbeans.dest.dir}/${nb.cluster.ide.dir} diff --git a/ide/css.editor/src/org/netbeans/modules/css/editor/indent/CssIndentTask.java b/ide/css.editor/src/org/netbeans/modules/css/editor/indent/CssIndentTask.java index e6bf8515df45..dfb9977ad26f 100644 --- a/ide/css.editor/src/org/netbeans/modules/css/editor/indent/CssIndentTask.java +++ b/ide/css.editor/src/org/netbeans/modules/css/editor/indent/CssIndentTask.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.LinkedList; import javax.swing.text.BadLocationException; import org.netbeans.api.lexer.Token; import org.netbeans.api.lexer.TokenHierarchy; @@ -94,37 +95,76 @@ private void doFormat() throws BadLocationException { .get(context.document()) .embeddedTokenSequences(reg.getStartOffset(), false); TokenSequence ts = tslist.get(tslist.size() - 1); - int blockLevel = determineBlocklevel(ts); + + LinkedList blockLevel = new LinkedList<>(); + + // Initialize blockLevel with the indentions created by the + // outside context + ts.moveStart(); + Token lastToken = null; + if (ts.moveNext()) { + List> tokenSequences = TokenHierarchy + .get(context.document()) + .tokenSequenceList(ts.languagePath(), 0, ts.offset()); + OUTER: + for (TokenSequence tsX : tokenSequences) { + tsX.moveStart(); + while (tsX.moveNext()) { + if (tsX.offset() >= ts.offset()) { + break OUTER; + } + if (tsX.token().id() == CssTokenId.LBRACE) { + if (isStringInterpolation(lastToken)) { + // The sequence "@{" and "#{" lead in a strong interpolation + // in LESS (former) and SCSS (latter). + blockLevel.addLast(IndentType.NONE); + } else { + blockLevel.addLast(IndentType.BLOCK); + } + } else if (tsX.token().id() == CssTokenId.RBRACE) { + blockLevel.pollLast(); + } + lastToken = tsX.token(); + } + } + } + ts.moveStart(); while(ts.moveNext()) { if(ts.token().id() == CssTokenId.LBRACE) { - blockLevel++; - // Ensure, that there is a newline after an block opening - // brace "{" - if(LexerUtils.followsToken(ts, CssTokenId.NL, false, true, CssTokenId.WS, CssTokenId.COMMENT) == null) { - while(ts.moveNext()) { - if (ts.token().id() != CssTokenId.WS && ts.token().id() != CssTokenId.COMMENT) { - newlinesMissing.add(ts.offset()); - ts.movePrevious(); - break; + if(! isStringInterpolation(lastToken)) { + blockLevel.add(IndentType.BLOCK); + // Ensure, that there is a newline after an block opening + // brace "{" + if(LexerUtils.followsToken(ts, CssTokenId.NL, false, true, CssTokenId.WS, CssTokenId.COMMENT) == null) { + while(ts.moveNext()) { + if (ts.token().id() != CssTokenId.WS && ts.token().id() != CssTokenId.COMMENT) { + newlinesMissing.add(ts.offset()); + ts.movePrevious(); + break; + } } } + } else { + blockLevel.add(IndentType.NONE); } } else if (ts.token().id() == CssTokenId.RBRACE) { - blockLevel--; - // Ensure, that there is a newline before an block closing - // brace "}" - if (LexerUtils.followsToken(ts, CssTokenId.NL, true, true, CssTokenId.WS, CssTokenId.COMMENT) == null) { - newlinesMissing.add(ts.offset()); - } - // Ensure, that there is a newline after an block closing - // brace "}" - if (LexerUtils.followsToken(ts, CssTokenId.NL, false, true, CssTokenId.WS, CssTokenId.COMMENT) == null) { - while(ts.moveNext()) { - if (ts.token().id() != CssTokenId.WS && ts.token().id() != CssTokenId.COMMENT) { - newlinesMissing.add(ts.offset()); - ts.movePrevious(); - break; + IndentType it = blockLevel.removeLast(); + if (it == IndentType.BLOCK) { + // Ensure, that there is a newline before an block closing + // brace "}" + if (LexerUtils.followsToken(ts, CssTokenId.NL, true, true, CssTokenId.WS, CssTokenId.COMMENT) == null) { + newlinesMissing.add(ts.offset()); + } + // Ensure, that there is a newline after an block closing + // brace "}" + if (LexerUtils.followsToken(ts, CssTokenId.NL, false, true, CssTokenId.WS, CssTokenId.COMMENT) == null) { + while (ts.moveNext()) { + if (ts.token().id() != CssTokenId.WS && ts.token().id() != CssTokenId.COMMENT) { + newlinesMissing.add(ts.offset()); + ts.movePrevious(); + break; + } } } } @@ -133,7 +173,7 @@ private void doFormat() throws BadLocationException { // (between property definitions). This should only be done // in regular CSS definitions (rules), but not in inline // style definitions - if (blockLevel > 0 && LexerUtils.followsToken(ts, asList(CssTokenId.NL, CssTokenId.RBRACE), false, true, CssTokenId.WS, CssTokenId.COMMENT) == null) { + if (!blockLevel.isEmpty() && LexerUtils.followsToken(ts, asList(CssTokenId.NL, CssTokenId.RBRACE), false, true, CssTokenId.WS, CssTokenId.COMMENT) == null) { while (ts.moveNext()) { if (ts.token().id() != CssTokenId.WS && ts.token().id() != CssTokenId.COMMENT) { newlinesMissing.add(ts.offset()); @@ -143,6 +183,7 @@ private void doFormat() throws BadLocationException { } } } + lastToken = ts.token(); } } // Remove newline insertions if @@ -162,6 +203,10 @@ private void doFormat() throws BadLocationException { } } + private static boolean isStringInterpolation(Token lastToken) { + return lastToken != null && (lastToken.id() == CssTokenId.AT_SIGN || lastToken.id() == CssTokenId.HASH_SYMBOL); + } + private boolean isPositionInFormatRegions(int pos) { for(Region r: context.indentRegions()) { if(r.getStartOffset() <= pos && r.getEndOffset() > pos) { @@ -170,31 +215,6 @@ private boolean isPositionInFormatRegions(int pos) { } return false; } - - private int determineBlocklevel(TokenSequence ts) { - int blockLevel = 0; - ts.moveStart(); - if (!ts.moveNext()) { - return 0; - } - List> tokenSequences = TokenHierarchy - .get(context.document()) - .tokenSequenceList(ts.languagePath(), 0, ts.offset()); - OUTER: for(TokenSequence tsX: tokenSequences) { - tsX.moveStart(); - while(tsX.moveNext()) { - if(tsX.offset() >= ts.offset()) { - break OUTER; - } - if(tsX.token().id() == CssTokenId.LBRACE) { - blockLevel++; - } else if (tsX.token().id() == CssTokenId.RBRACE) { - blockLevel--; - } - } - } - return blockLevel; - } @Override public ExtraLock indentLock() { @@ -206,4 +226,8 @@ public Lookup getLookup() { return lookup; } + private static enum IndentType { + NONE, + BLOCK + } } diff --git a/ide/css.editor/test/unit/data/testfiles/case005.less b/ide/css.editor/test/unit/data/testfiles/case005.less new file mode 100644 index 000000000000..88439f0fa636 --- /dev/null +++ b/ide/css.editor/test/unit/data/testfiles/case005.less @@ -0,0 +1 @@ +.@{my-selector} { font-weight: bold; line-height: 40px; margin: 0 auto; } diff --git a/ide/css.editor/test/unit/data/testfiles/case005.less.formatted b/ide/css.editor/test/unit/data/testfiles/case005.less.formatted new file mode 100644 index 000000000000..f6648e6cb781 --- /dev/null +++ b/ide/css.editor/test/unit/data/testfiles/case005.less.formatted @@ -0,0 +1,5 @@ +.@{my-selector} { + font-weight: bold; + line-height: 40px; + margin: 0 auto; +} \ No newline at end of file diff --git a/ide/css.editor/test/unit/data/testfiles/case006.scss b/ide/css.editor/test/unit/data/testfiles/case006.scss new file mode 100644 index 000000000000..5abfb9a11085 --- /dev/null +++ b/ide/css.editor/test/unit/data/testfiles/case006.scss @@ -0,0 +1,6 @@ +@mixin corner-icon($name, $top-or-bottom, $left-or-right) { + .icon-#{$name} { + background-image: url("/icons/#{$name}.svg"); position: absolute; + #{$top-or-bottom}: 0; #{$left-or-right}: 0; + } +} \ No newline at end of file diff --git a/ide/css.editor/test/unit/data/testfiles/case006.scss.formatted b/ide/css.editor/test/unit/data/testfiles/case006.scss.formatted new file mode 100644 index 000000000000..3b21956c78d9 --- /dev/null +++ b/ide/css.editor/test/unit/data/testfiles/case006.scss.formatted @@ -0,0 +1,8 @@ +@mixin corner-icon($name, $top-or-bottom, $left-or-right) { + .icon-#{$name} { + background-image: url("/icons/#{$name}.svg"); + position: absolute; + #{$top-or-bottom}: 0; + #{$left-or-right}: 0; + } +} \ No newline at end of file diff --git a/ide/css.editor/test/unit/src/org/netbeans/modules/css/editor/indent/CssIndenterTest.java b/ide/css.editor/test/unit/src/org/netbeans/modules/css/editor/indent/CssIndenterTest.java index 609d8634e0c0..1e4c423e3488 100644 --- a/ide/css.editor/test/unit/src/org/netbeans/modules/css/editor/indent/CssIndenterTest.java +++ b/ide/css.editor/test/unit/src/org/netbeans/modules/css/editor/indent/CssIndenterTest.java @@ -186,7 +186,7 @@ public void testFormattingNetBeansCSS() throws Exception { public void testPartitialFormatting() throws Exception { IndentPrefs preferences = new IndentPrefs(4, 4); String file = "testfiles/partialformatting.css"; - + FileObject fo = getTestFile(file); assertNotNull(fo); BaseDocument doc = getDocument(fo); @@ -201,14 +201,14 @@ public void testPartitialFormatting() throws Exception { assertDescriptionMatches(file, after, false, ".formatted"); DataObject.find(fo).getLookup().lookup(Closable.class).close(); - + doc = getDocument(fo); setupDocumentIndentation(doc, preferences); format(doc, formatter, 857, 904, false); after = doc.getText(0, doc.getLength()); assertDescriptionMatches(file, after, false, ".formatted2"); } - + public void testIndentation() throws Exception { // property indentation: insertNewline("a{^background: red;\n }\n", "a{\n ^background: red;\n }\n", null); diff --git a/ide/css.editor/test/unit/src/org/netbeans/modules/css/editor/indent/LessIndenterTest.java b/ide/css.editor/test/unit/src/org/netbeans/modules/css/editor/indent/LessIndenterTest.java new file mode 100644 index 000000000000..0d4559658577 --- /dev/null +++ b/ide/css.editor/test/unit/src/org/netbeans/modules/css/editor/indent/LessIndenterTest.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.netbeans.modules.css.editor.indent; + +import org.netbeans.modules.css.editor.test.*; +import org.netbeans.modules.csl.api.Formatter; +import org.netbeans.modules.csl.api.test.CslTestBase; +import org.netbeans.modules.csl.spi.DefaultLanguageConfig; +import org.netbeans.modules.css.editor.csl.CssLanguage; + + +public class LessIndenterTest extends CslTestBase { + + private static final String PROP_MIME_TYPE = "mimeType"; //NOI18N + + public LessIndenterTest(String name) { + super(name); + } + + @Override + protected DefaultLanguageConfig getPreferredLanguage() { + return new CssLanguage(); + } + + @Override + protected String getPreferredMimeType() { + return "text/less"; + } + + @Override + public Formatter getFormatter(IndentPrefs preferences) { + return null; + } + + public void testFormattingCase5() throws Exception { + reformatFileContents("testfiles/case005.less", new IndentPrefs(4, 4)); + } + +} diff --git a/ide/css.editor/test/unit/src/org/netbeans/modules/css/editor/indent/ScssIndenterTest.java b/ide/css.editor/test/unit/src/org/netbeans/modules/css/editor/indent/ScssIndenterTest.java new file mode 100644 index 000000000000..66901eb3c9b7 --- /dev/null +++ b/ide/css.editor/test/unit/src/org/netbeans/modules/css/editor/indent/ScssIndenterTest.java @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.netbeans.modules.css.editor.indent; + +import org.netbeans.editor.BaseDocument; +import org.netbeans.modules.csl.api.Formatter; +import org.netbeans.modules.csl.api.test.CslTestBase; +import org.netbeans.modules.csl.spi.DefaultLanguageConfig; +import org.netbeans.modules.css.editor.csl.CssLanguage; +import org.netbeans.modules.css.lib.TestUtil; +import org.netbeans.modules.parsing.api.Source; +import org.openide.filesystems.FileObject; + + +public class ScssIndenterTest extends CslTestBase { + + private static final String PROP_MIME_TYPE = "mimeType"; //NOI18N + + public ScssIndenterTest(String name) { + super(name); + } + + @Override + protected DefaultLanguageConfig getPreferredLanguage() { + return new CssLanguage(); + } + + @Override + protected String getPreferredMimeType() { + return "text/scss"; + } + + @Override + public Formatter getFormatter(IndentPrefs preferences) { + return null; + } + + public void testFormattingCase6() throws Exception { + reformatFileContents("testfiles/case006.scss", new IndentPrefs(4, 4)); + } + +}