diff --git a/qt/org.eclipse.cdt.qt.ui.tests/src/org/eclipse/cdt/qt/pro/parser/tests/QtProjectFileModifierTest.java b/qt/org.eclipse.cdt.qt.ui.tests/src/org/eclipse/cdt/qt/pro/parser/tests/QtProjectFileModifierTest.java new file mode 100644 index 00000000000..4880a08eed9 --- /dev/null +++ b/qt/org.eclipse.cdt.qt.ui.tests/src/org/eclipse/cdt/qt/pro/parser/tests/QtProjectFileModifierTest.java @@ -0,0 +1,340 @@ +/******************************************************************************* + * Copyright (c) 2015 QNX Software Systems and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * QNX Software Systems - Initial API and implementation + *******************************************************************************/ +package org.eclipse.cdt.qt.pro.parser.tests; + +import org.eclipse.cdt.internal.qt.ui.pro.parser.QtProjectFileModifier; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; +import org.junit.Test; + +import junit.framework.TestCase; + +public class QtProjectFileModifierTest extends TestCase { + + @Test + public void test_ReplaceValue_SingleValue() { + IDocument document = new Document("SOURCES += main.cpp"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + + assertTrue(modifier.replaceVariableValue("SOURCES", "main.cpp", "main2.cpp")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + assertEquals("SOURCES += main2.cpp", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_ReplaceValue_HasCommentOnMainLine() { + IDocument document = new Document("SOURCES += main.cpp # This is a comment"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + + assertTrue(modifier.replaceVariableValue("SOURCES", "main.cpp", "main2.cpp")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + assertEquals("SOURCES += main2.cpp # This is a comment", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_ReplaceValue_HasCommentOnSubsequentLine() { + IDocument document = new Document( + "SOURCES += main.cpp \\ # This is a comment\n" //$NON-NLS-1$ + + " main2.cpp # This is a comment"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + + assertTrue(modifier.replaceVariableValue("SOURCES", "main2.cpp", "main3.cpp")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + assertEquals( + "SOURCES += main.cpp \\ # This is a comment\n" //$NON-NLS-1$ + + " main3.cpp # This is a comment", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_ReplaceValue_MatchWholeLineFalse() { + IDocument document = new Document("CONFIG = qt debug"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + + assertTrue(modifier.replaceVariableValue("CONFIG", "debug", "console", false)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + assertEquals( + "CONFIG = qt console", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_ReplaceValue_DoesNotExist() { + IDocument document = new Document("CONFIG = qt debug"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + + assertFalse(modifier.replaceVariableValue("CONFIG", "console", "debug", false)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + assertEquals( + "CONFIG = qt debug", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_ReplaceMultilineValue_MatchWholeLineFalse() { + IDocument document = new Document( + "CONFIG = qt \\\n" //$NON-NLS-1$ + + " debug"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + + assertTrue(modifier.replaceVariableValue("CONFIG", "debug", "console", false)); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + assertEquals( + "CONFIG = qt \\\n" //$NON-NLS-1$ + + " console", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_ReplaceMultilineValue() { + IDocument document = new Document( + "SOURCES += main.cpp \\\n" //$NON-NLS-1$ + + " main2.cpp"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + + assertTrue(modifier.replaceVariableValue("SOURCES", "main2.cpp", "main3.cpp")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + assertEquals( + "SOURCES += main.cpp \\\n" //$NON-NLS-1$ + + " main3.cpp", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_ReplaceMultilineValue_HasComment() { + IDocument document = new Document( + "SOURCES += main.cpp \\\n" //$NON-NLS-1$ + + " main2.cpp # This is a comment"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + + assertTrue(modifier.replaceVariableValue("SOURCES", "main2.cpp", "main3.cpp")); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + assertEquals( + "SOURCES += main.cpp \\\n" //$NON-NLS-1$ + + " main3.cpp # This is a comment", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_AddValue() { + IDocument document = new Document("SOURCES += main.cpp"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + modifier.addVariableValue("SOURCES", "main2.cpp"); //$NON-NLS-1$ //$NON-NLS-2$ + + assertEquals( + "SOURCES += main.cpp \\\n" //$NON-NLS-1$ + + " main2.cpp", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_AddValue_NoIndentation() { + IDocument document = new Document( + "SOURCES += main.cpp \\\n" //$NON-NLS-1$ + + "noindent.cpp"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + modifier.addVariableValue("SOURCES", "main2.cpp"); //$NON-NLS-1$ //$NON-NLS-2$ + + assertEquals( + "SOURCES += main.cpp \\\n" //$NON-NLS-1$ + + "noindent.cpp \\\n" //$NON-NLS-1$ + + "main2.cpp", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_AddValue_AlreadyExists() { + IDocument document = new Document("SOURCES += main.cpp"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + modifier.addVariableValue("SOURCES", "main.cpp"); //$NON-NLS-1$ //$NON-NLS-2$ + + assertEquals("SOURCES += main.cpp", document.get()); //$NON-NLS-1$ + } + + @Test + public void test_AddValue_HasCommentOnMainLine() { + IDocument document = new Document("SOURCES += main.cpp # This is a comment"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + modifier.addVariableValue("SOURCES", "main2.cpp"); //$NON-NLS-1$ //$NON-NLS-2$ + + assertEquals( + "SOURCES += main.cpp \\ # This is a comment\n" //$NON-NLS-1$ + + " main2.cpp", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_AddValue_HasCommentOnSubsequentLine() { + IDocument document = new Document( + "SOURCES += main.cpp \\ # This is a comment \n" //$NON-NLS-1$ + + " main2.cpp # this is a comment\n\n"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + modifier.addVariableValue("SOURCES", "main3.cpp"); //$NON-NLS-1$ //$NON-NLS-2$ + + assertEquals( + "SOURCES += main.cpp \\ # This is a comment \n" //$NON-NLS-1$ + + " main2.cpp \\ # this is a comment\n" //$NON-NLS-1$ + + " main3.cpp\n\n", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_AddValue_CommentIndentation() { + IDocument document = new Document( + "SOURCES += main.cpp \\ # Test comment\n" //$NON-NLS-1$ + + " main2.cpp \\ # Test comment2\n" //$NON-NLS-1$ + + " main3.cpp # Test comment3"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + modifier.addVariableValue("SOURCES", "main4.cpp"); //$NON-NLS-1$ //$NON-NLS-2$ + + assertEquals( + "SOURCES += main.cpp \\ # Test comment\n" //$NON-NLS-1$ + + " main2.cpp \\ # Test comment2\n" //$NON-NLS-1$ + + " main3.cpp \\ # Test comment3\n" //$NON-NLS-1$ + + " main4.cpp", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_AddValue_MultipleVariables() { + IDocument document = new Document( + "SOURCES += main.cpp\n" //$NON-NLS-1$ + + "\n" //$NON-NLS-1$ + + "QT = app"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + modifier.addVariableValue("SOURCES", "main2.cpp"); //$NON-NLS-1$ //$NON-NLS-2$ + + assertEquals( + "SOURCES += main.cpp \\\n" + //$NON-NLS-1$ + " main2.cpp\n" + //$NON-NLS-1$ + "\n" + //$NON-NLS-1$ + "QT = app", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_AddValue_EmptyDocument() { + IDocument document = new Document("\t \n\n\t\n\n\n\n"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + modifier.addVariableValue("SOURCES", "main.cpp"); //$NON-NLS-1$ //$NON-NLS-2$ + + assertEquals( + "SOURCES += main.cpp\n", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_AddValue_VariableDoesNotExist() { + IDocument document = new Document("CONFIG += qt debug"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + modifier.addVariableValue("SOURCES", "main.cpp"); //$NON-NLS-1$ //$NON-NLS-2$ + + assertEquals( + "CONFIG += qt debug\n" //$NON-NLS-1$ + + "\n" //$NON-NLS-1$ + + "SOURCES += main.cpp\n", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_AddValue_VariableDoesNotExist2() { + IDocument document = new Document("CONFIG += qt debug\n"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + modifier.addVariableValue("SOURCES", "main.cpp"); //$NON-NLS-1$ //$NON-NLS-2$ + + assertEquals( + "CONFIG += qt debug\n" //$NON-NLS-1$ + + "\n" //$NON-NLS-1$ + + "SOURCES += main.cpp\n", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_AddValue_VariableDoesNotExist3() { + IDocument document = new Document("CONFIG += qt debug\n\n"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + modifier.addVariableValue("SOURCES", "main.cpp"); //$NON-NLS-1$ //$NON-NLS-2$ + + assertEquals( + "CONFIG += qt debug\n" //$NON-NLS-1$ + + "\n" //$NON-NLS-1$ + + "\n" //$NON-NLS-1$ + + "SOURCES += main.cpp\n", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_RemoveThenAddValue() { + IDocument document = new Document( + "SOURCES += main.cpp \\\n" //$NON-NLS-1$ + + " main2.cpp \\\n" //$NON-NLS-1$ + + " main3.cpp\n"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + modifier.removeVariableValue("SOURCES", "main3.cpp"); //$NON-NLS-1$ //$NON-NLS-2$ + modifier.addVariableValue("SOURCES", "main4.cpp"); //$NON-NLS-1$ //$NON-NLS-2$ + + assertEquals( + "SOURCES += main.cpp \\\n" //$NON-NLS-1$ + + " main2.cpp \\\n" //$NON-NLS-1$ + + " main4.cpp\n", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_RemoveValue_FirstLine() { + IDocument document = new Document( + "SOURCES += main.cpp \\ # Test comment\n" //$NON-NLS-1$ + + " main2.cpp \\ # Test comment2\n" //$NON-NLS-1$ + + " main3.cpp \\ # Test comment3\n" //$NON-NLS-1$ + + " main4.cpp # Test comment4"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + modifier.removeVariableValue("SOURCES", "main.cpp"); //$NON-NLS-1$ //$NON-NLS-2$ + + assertEquals( + "SOURCES += main2.cpp \\ # Test comment2\n" //$NON-NLS-1$ + + " main3.cpp \\ # Test comment3\n" //$NON-NLS-1$ + + " main4.cpp # Test comment4", //$NON-NLS-1$ + document.get()); + } + + @Test + public void test_RemoveValue_MiddleLine() { + IDocument document = new Document( + "SOURCES += main.cpp \\ # Test comment\n" //$NON-NLS-1$ + + " main2.cpp \\ # Test comment2\n" //$NON-NLS-1$ + + " main3.cpp \\ # Test comment3\n" //$NON-NLS-1$ + + " main4.cpp # Test comment4"); //$NON-NLS-1$ + + QtProjectFileModifier modifier = new QtProjectFileModifier(document); + modifier.removeVariableValue("SOURCES", "main2.cpp"); //$NON-NLS-1$ //$NON-NLS-2$ + + assertEquals( + "SOURCES += main.cpp \\ # Test comment\n" //$NON-NLS-1$ + + " main3.cpp \\ # Test comment3\n" //$NON-NLS-1$ + + " main4.cpp # Test comment4", //$NON-NLS-1$ + document.get()); + } +} diff --git a/qt/org.eclipse.cdt.qt.ui.tests/src/org/eclipse/cdt/qt/pro/parser/tests/QtProjectFileParserTest.java b/qt/org.eclipse.cdt.qt.ui.tests/src/org/eclipse/cdt/qt/pro/parser/tests/QtProjectFileParserTest.java new file mode 100644 index 00000000000..0b2bca1b5ee --- /dev/null +++ b/qt/org.eclipse.cdt.qt.ui.tests/src/org/eclipse/cdt/qt/pro/parser/tests/QtProjectFileParserTest.java @@ -0,0 +1,181 @@ +/******************************************************************************* + * Copyright (c) 2015 QNX Software Systems and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * QNX Software Systems - Initial API and implementation + *******************************************************************************/ +package org.eclipse.cdt.qt.pro.parser.tests; + +import java.util.List; + +import org.eclipse.cdt.internal.qt.ui.pro.parser.QtProjectFileParser; +import org.eclipse.cdt.internal.qt.ui.pro.parser.QtProjectVariable; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; +import org.junit.Test; + +import junit.framework.TestCase; + +public class QtProjectFileParserTest extends TestCase { + + @Test + public void test_AssignmentOperator_Equals() { + IDocument document = new Document("SOURCES = main.cpp"); //$NON-NLS-1$ + QtProjectFileParser parser = new QtProjectFileParser(document); + + List variables = parser.getAllVariables(); + assertFalse("Unable to parse variable", variables.isEmpty()); //$NON-NLS-1$ + assertEquals("Invalid assignment operator", "=", variables.get(0).getAssignmentOperator()); //$NON-NLS-1$ //$NON-NLS-2$ + } + + @Test + public void test_AssignmentOperator_PlusEquals() { + IDocument document = new Document("SOURCES += main.cpp"); //$NON-NLS-1$ + QtProjectFileParser parser = new QtProjectFileParser(document); + + List variables = parser.getAllVariables(); + assertFalse("Unable to parse variable", variables.isEmpty()); //$NON-NLS-1$ + assertEquals("Invalid assignment operator", "+=", variables.get(0).getAssignmentOperator()); //$NON-NLS-1$ //$NON-NLS-2$ + } + + @Test + public void test_AssignmentOperator_MinusEquals() { + IDocument document = new Document("SOURCES -= main.cpp"); //$NON-NLS-1$ + QtProjectFileParser parser = new QtProjectFileParser(document); + + List variables = parser.getAllVariables(); + assertFalse("Unable to parse variable", variables.isEmpty()); //$NON-NLS-1$ + assertEquals("Invalid assignment operator", "-=", variables.get(0).getAssignmentOperator()); //$NON-NLS-1$ //$NON-NLS-2$ + } + + @Test + public void test_AssignmentOperator_AsterixEquals() { + IDocument document = new Document("SOURCES *= main.cpp"); //$NON-NLS-1$ + QtProjectFileParser parser = new QtProjectFileParser(document); + + List variables = parser.getAllVariables(); + assertFalse("Unable to parse variable", variables.isEmpty()); //$NON-NLS-1$ + assertEquals("Invalid assignment operator", "*=", variables.get(0).getAssignmentOperator()); //$NON-NLS-1$ //$NON-NLS-2$ + } + + @Test + public void test_CommentedVariable() { + IDocument document = new Document("# SOURCES += main.cpp"); //$NON-NLS-1$ + QtProjectFileParser parser = new QtProjectFileParser(document); + assertTrue("Found variable even though it was commented", parser.getAllVariables().isEmpty()); //$NON-NLS-1$ + } + + @Test + public void test_CommentedVariable2() { + IDocument document = new Document("SOURCES # += main.cpp"); //$NON-NLS-1$ + QtProjectFileParser parser = new QtProjectFileParser(document); + assertTrue("Found variable even though it was commented", parser.getAllVariables().isEmpty()); //$NON-NLS-1$ + } + + @Test + public void test_MalformedVariable() { + IDocument document = new Document("MY VARIABLE # += main.cpp"); //$NON-NLS-1$ + QtProjectFileParser parser = new QtProjectFileParser(document); + assertTrue("Found variable even though it was malformed", parser.getAllVariables().isEmpty()); //$NON-NLS-1$ + } + + @Test + public void test_MalformedVariable2() { + IDocument document = new Document("\\SOURCES # += main.cpp"); //$NON-NLS-1$ + QtProjectFileParser parser = new QtProjectFileParser(document); + assertTrue("Found variable even though it was malformed", parser.getAllVariables().isEmpty()); //$NON-NLS-1$ + } + + @Test + public void test_FullyQualifiedName() { + IDocument document = new Document("fully.qualified.Name += main.cpp"); //$NON-NLS-1$ + QtProjectFileParser parser = new QtProjectFileParser(document); + + QtProjectVariable sources = parser.getVariable("fully.qualified.Name"); //$NON-NLS-1$ + assertNotNull("Unable to parse variable", sources); //$NON-NLS-1$ + } + + @Test + public void test_SingleLineVariable() { + IDocument document = new Document("SOURCES += main.cpp"); //$NON-NLS-1$ + QtProjectFileParser parser = new QtProjectFileParser(document); + + QtProjectVariable sources = parser.getVariable("SOURCES"); //$NON-NLS-1$ + assertNotNull("Unable to parse variable", sources); //$NON-NLS-1$ + assertTrue("Unable to parse \"main.cpp\" from SOURCES variable", sources.getValueIndex("main.cpp") == 0); //$NON-NLS-1$ //$NON-NLS-2$ + } + + @Test + public void test_SingleLineVariable_MultipleValues() { + IDocument document = new Document("CONFIG += qt debug"); //$NON-NLS-1$ + QtProjectFileParser parser = new QtProjectFileParser(document); + + QtProjectVariable sources = parser.getVariable("CONFIG"); //$NON-NLS-1$ + assertNotNull("Unable to parse variable", sources); //$NON-NLS-1$ + assertTrue("Unable to parse \"qt debug\" from SOURCES variable", sources.getValueIndex("qt debug") == 0); //$NON-NLS-1$ //$NON-NLS-2$ + } + + @Test + public void test_VariableWithComment() { + IDocument document = new Document("SOURCES += main.cpp # this is a comment\n"); //$NON-NLS-1$ + QtProjectFileParser parser = new QtProjectFileParser(document); + + QtProjectVariable sources = parser.getVariable("SOURCES"); //$NON-NLS-1$ + assertNotNull("Unable to parse variable", sources); //$NON-NLS-1$ + assertEquals("Unable to parse assignment from SOURCES variable", "+=", sources.getAssignmentOperator()); //$NON-NLS-1$ //$NON-NLS-2$ + assertTrue("Unable to parse \"main.cpp\" from SOURCES variable", sources.getValueIndex("main.cpp") == 0); //$NON-NLS-1$ //$NON-NLS-2$ + } + + @Test + public void test_MultilineVariable() { + IDocument document = new Document("SOURCES += main.cpp \\\n main2.cpp"); //$NON-NLS-1$ + QtProjectFileParser parser = new QtProjectFileParser(document); + + QtProjectVariable sources = parser.getVariable("SOURCES"); //$NON-NLS-1$ + assertNotNull("Unable to parse variable", sources); //$NON-NLS-1$ + assertEquals("Incorrect number of lines", sources.getNumberOfLines(), 2); //$NON-NLS-1$ + assertTrue("Unable to parse \"main.cpp\" from SOURCES variable", sources.getValueIndex("main.cpp") == 0); //$NON-NLS-1$ //$NON-NLS-2$ + assertTrue("Unable to parse \"main2.cpp\" from SOURCES variable", sources.getValueIndex("main2.cpp") == 1); //$NON-NLS-1$ //$NON-NLS-2$ + } + + @Test + public void test_MultilineVariable2() { + IDocument document = new Document("SOURCES += main.cpp \\\n main2.cpp \\\n main3.cpp"); //$NON-NLS-1$ + QtProjectFileParser parser = new QtProjectFileParser(document); + + QtProjectVariable sources = parser.getVariable("SOURCES"); //$NON-NLS-1$ + assertNotNull("Unable to parse variable", sources); //$NON-NLS-1$ + assertEquals("Incorrect number of lines", 3, sources.getNumberOfLines()); //$NON-NLS-1$ + assertTrue("Unable to parse \"main.cpp\" from SOURCES variable", sources.getValueIndex("main.cpp") == 0); //$NON-NLS-1$ //$NON-NLS-2$ + assertTrue("Unable to parse \"main2.cpp\" from SOURCES variable", sources.getValueIndex("main2.cpp") == 1); //$NON-NLS-1$ //$NON-NLS-2$ + assertTrue("Unable to parse \"main3.cpp\" from SOURCES variable", sources.getValueIndex("main3.cpp") == 2); //$NON-NLS-1$ //$NON-NLS-2$ + } + + @Test + public void test_MalformedMultilineVariable() { + IDocument document = new Document("SOURCES += main.cpp \\\n main2.cpp \\"); //$NON-NLS-1$ + QtProjectFileParser parser = new QtProjectFileParser(document); + + QtProjectVariable sources = parser.getVariable("SOURCES"); //$NON-NLS-1$ + assertNotNull("Unable to parse variable", sources); //$NON-NLS-1$ + assertEquals("Incorrect number of lines", 2, sources.getNumberOfLines()); //$NON-NLS-1$ + assertTrue("Unable to parse \"main.cpp\" from SOURCES variable", sources.getValueIndex("main.cpp") == 0); //$NON-NLS-1$ //$NON-NLS-2$ + assertTrue("Unable to parse \"main2.cpp\" from SOURCES variable", sources.getValueIndex("main2.cpp") == 1); //$NON-NLS-1$ //$NON-NLS-2$ + } + + @Test + public void test_MultilineVariable_WithComment() { + IDocument document = new Document("SOURCES += main.cpp \\ # this is a comment \n main2.cpp # this is a comment "); //$NON-NLS-1$ + QtProjectFileParser parser = new QtProjectFileParser(document); + + QtProjectVariable sources = parser.getVariable("SOURCES"); //$NON-NLS-1$ + assertNotNull("Unable to parse variable", sources); //$NON-NLS-1$ + assertEquals("Incorrect number of lines", 2, sources.getNumberOfLines()); //$NON-NLS-1$ + assertTrue("Unable to parse \"main.cpp\" from SOURCES variable", sources.getValueIndex("main.cpp") == 0); //$NON-NLS-1$ //$NON-NLS-2$ + assertTrue("Unable to parse \"main2.cpp\" from SOURCES variable", sources.getValueIndex("main2.cpp") == 1); //$NON-NLS-1$ //$NON-NLS-2$ + } +} diff --git a/qt/org.eclipse.cdt.qt.ui.tests/src/org/eclipse/cdt/qt/tests/AllQtTests.java b/qt/org.eclipse.cdt.qt.ui.tests/src/org/eclipse/cdt/qt/tests/AllQtTests.java index dc3ff0d4478..9fd28fe9559 100644 --- a/qt/org.eclipse.cdt.qt.ui.tests/src/org/eclipse/cdt/qt/tests/AllQtTests.java +++ b/qt/org.eclipse.cdt.qt.ui.tests/src/org/eclipse/cdt/qt/tests/AllQtTests.java @@ -7,6 +7,9 @@ */ package org.eclipse.cdt.qt.tests; +import org.eclipse.cdt.qt.pro.parser.tests.QtProjectFileModifierTest; +import org.eclipse.cdt.qt.pro.parser.tests.QtProjectFileParserTest; + import junit.framework.Test; import junit.framework.TestSuite; @@ -22,6 +25,8 @@ public class AllQtTests extends TestSuite { QtContentAssistantTests.class, QtIndexTests.class, QtRegressionTests.class, - QmlRegistrationTests.class); + QmlRegistrationTests.class, + QtProjectFileModifierTest.class, + QtProjectFileParserTest.class); } } diff --git a/qt/org.eclipse.cdt.qt.ui/META-INF/MANIFEST.MF b/qt/org.eclipse.cdt.qt.ui/META-INF/MANIFEST.MF index d4e4fc2c82e..059cc598a18 100644 --- a/qt/org.eclipse.cdt.qt.ui/META-INF/MANIFEST.MF +++ b/qt/org.eclipse.cdt.qt.ui/META-INF/MANIFEST.MF @@ -19,4 +19,5 @@ Require-Bundle: org.eclipse.core.runtime, org.eclipse.cdt.qt.core Bundle-RequiredExecutionEnvironment: JavaSE-1.7 Bundle-ActivationPolicy: lazy -Export-Package: org.eclipse.cdt.internal.qt.ui.assist;x-friends:="org.eclipse.cdt.qt.ui.tests" +Export-Package: org.eclipse.cdt.internal.qt.ui.assist;x-friends:="org.eclipse.cdt.qt.ui.tests", + org.eclipse.cdt.internal.qt.ui.pro.parser;x-friends:="org.eclipse.cdt.qt.ui.tests" diff --git a/qt/org.eclipse.cdt.qt.ui/plugin.xml b/qt/org.eclipse.cdt.qt.ui/plugin.xml index a46e2ccd46e..8037089b844 100644 --- a/qt/org.eclipse.cdt.qt.ui/plugin.xml +++ b/qt/org.eclipse.cdt.qt.ui/plugin.xml @@ -59,7 +59,7 @@ diff --git a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/QtUIPlugin.java b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/QtUIPlugin.java index 8e2daf6091a..4851fca8712 100644 --- a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/QtUIPlugin.java +++ b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/QtUIPlugin.java @@ -8,6 +8,11 @@ package org.eclipse.cdt.internal.qt.ui; import org.eclipse.cdt.core.model.CModelException; +import org.eclipse.cdt.internal.qt.ui.resources.QtResourceChangeListener; +import org.eclipse.cdt.internal.qt.ui.resources.QtWorkspaceSaveParticipant; +import org.eclipse.core.resources.ISaveParticipant; +import org.eclipse.core.resources.ISavedState; +import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; @@ -44,6 +49,15 @@ public class QtUIPlugin extends AbstractUIPlugin { public void start(BundleContext context) throws Exception { super.start(context); plugin = this; + + // Use a save participant to grab any changed resources while this plugin was inactive + QtResourceChangeListener resourceManager = new QtResourceChangeListener(); + ISaveParticipant saveParticipant = new QtWorkspaceSaveParticipant(); + ISavedState lastState = ResourcesPlugin.getWorkspace().addSaveParticipant(QtUIPlugin.PLUGIN_ID, saveParticipant); + if (lastState != null) { + lastState.processResourceChangeEvents(resourceManager); + } + ResourcesPlugin.getWorkspace().addResourceChangeListener(resourceManager); } @Override diff --git a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/pro/parser/QtProjectFileModifier.java b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/pro/parser/QtProjectFileModifier.java new file mode 100644 index 00000000000..d09a646e316 --- /dev/null +++ b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/pro/parser/QtProjectFileModifier.java @@ -0,0 +1,286 @@ +/******************************************************************************* + * Copyright (c) 2015 QNX Software Systems and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * QNX Software Systems - Initial API and implementation + *******************************************************************************/ +package org.eclipse.cdt.internal.qt.ui.pro.parser; + +import org.eclipse.cdt.internal.qt.core.QtPlugin; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; + +/** + * Allows for the manipulation of information stored in a Qt Project File. At the moment the only modifiable information is that + * which is contained within variables such as the following: + * + *
+ * SOURCES += file.cpp \ # This is the first line with value "file.cpp"
+ *     file2.cpp # This is the second line with value "file2.cpp"
+ * 
+ * + * This class supports the following modifications to variables: + *
    + *
  • Add Value: If the specified String does not exist in the given variable then it is added as a new line at the end of + * the variable declaration. A line escape (\) is also inserted into the preceding line.
  • + *
  • Remove Value: If the specified String exists in the given variable then it is removed. The line escape character (\) + * is also removed from the preceding line if necessary.
  • + *
  • Replace Value: If the specified String exists as a line in the given variable, then it is replaced with another + * String. All spacing is preserved as only the value itself is modified.
  • + *
+ *

+ * Comments may appear after the line escape character (\) in a variable Declaration. For this case, replace and addition operations + * will preserve these comments. However, a comment will not be preserved if its line is deleted during a remove operation. + *

+ */ +public class QtProjectFileModifier { + + private QtProjectFileParser parser; + private IDocument document; + + public QtProjectFileModifier(IDocument doc) { + if (doc == null) { + throw new IllegalArgumentException("document cannot be null"); //$NON-NLS-1$ + } + + this.document = doc; + this.parser = new QtProjectFileParser(doc); + } + + public QtProjectFileModifier(QtProjectFileParser parser) { + if (parser == null) { + throw new IllegalArgumentException("parser cannot be null"); //$NON-NLS-1$ + } + + this.document = parser.getDocument(); + this.parser = parser; + } + + /** + * Attempts to replace the given value with a new value if it is found within the given variable name. This is a convenience + * method equivalent to replaceVariableValue(variable,oldValue,newValue,true) and will only match values that + * occupy an entire line within the variable declaration. + *

+ * This method does not create a new value if the specified oldValue was not found. If this behavior is + * desired, then check for a return of false from this method and then call the addVariableValue + * method. + *

+ *

+ * Note: The "entire line" refers to only the value as it appears in the variable declaration. That is, any whitespace + * before or after will not be included when matching a value to the "entire line". + *

+ * + * @param variable + * the name of the variable + * @param oldValue + * the value that will be replaced + * @param newValue + * the value to replace with + * @return whether or not the value was able to be replaced + */ + public boolean replaceVariableValue(String variable, String oldValue, String newValue) { + return replaceVariableValue(variable, oldValue, newValue, true); + } + + /** + * Attempts to replace the first instance of oldValue with newValue if it is found within the given + * variable name. If matchWholeLine is false, this method will try to match sections of each line with the value of + * oldValue. If a match is found, only that portion of the line will be replaced. If matchWholeLine is + * true, this method will try to match the entire line with the value of oldValue and will replace that. All other + * line spacing and comments are preserved as only the value itself is replaced. + *

+ * This method does not create a new value if oldValue was not found. If this behavior is desired, then + * check for a return of false from this method and then call the addVariableValue method. + *

+ *

+ * Note: The "entire line" refers to only the value as it appears in the variable declaration. That is, any whitespace + * before or after will not be included when matching a value to the "entire line". + *

+ * + * @param variable + * the name of the variable + * @param oldValue + * the value that will be replaced + * @param newValue + * the value to replace with + * @param matchWholeLine + * whether or not the value should match the entire line + * @return whether or not the value was able to be replaced + */ + public boolean replaceVariableValue(String variable, String oldValue, String newValue, boolean matchWholeLine) { + QtProjectVariable var = parser.getVariable(variable); + + if (var != null) { + if (matchWholeLine) { + int line = var.getValueIndex(oldValue); + if (line >= 0) { + return replaceVariableValue(var, line, newValue); + } + } else { + int line = 0; + for (String value : var.getValues()) { + int offset = value.indexOf(oldValue); + if (offset >= 0) { + return replaceVariableValue(var, + line, + var.getValueOffsetForLine(line) + offset, + oldValue.length(), + newValue); + } + line++; + } + } + } + return false; + } + + private boolean replaceVariableValue(QtProjectVariable var, int lineNo, String newValue) { + int offset = var.getValueOffsetForLine(lineNo); + String value = var.getValueForLine(lineNo); + int length = value.length(); + + return replaceVariableValue(var, lineNo, offset, length, newValue); + } + + private boolean replaceVariableValue(QtProjectVariable var, int lineNo, int offset, int length, String newValue) { + try { + document.replace(offset, length, newValue); + return true; + } catch (BadLocationException e) { + QtPlugin.log(e); + } + return false; + } + + /** + * Adds value to the specified variable as a new line and escapes the previous line with a backslash. The escaping + * is done in such a way that comments and spacing are preserved on the previous line. If this variable does not exist, a new + * one is created at the bottom-most position of the document with the initial value specified by value. + * + * @param variable + * the name of the variable to add to + * @param value + * the value to add to the variable + */ + public void addVariableValue(String variable, String value) { + QtProjectVariable var = parser.getVariable(variable); + + if (var != null) { + if (var.getValueIndex(value) < 0) { + int line = var.getNumberOfLines() - 1; + String indent = var.getIndentString(line); + + int offset = var.getEndOffset(); + if (var.getLine(line).endsWith("\n")) { //$NON-NLS-1$ + offset--; + } + + try { + document.replace(offset, 0, "\n" + indent + value); //$NON-NLS-1$ + } catch (BadLocationException e) { + QtPlugin.log(e); + } + + try { + offset = var.getLineEscapeReplacementOffset(line); + String lineEscape = var.getLineEscapeReplacementString(line); + + document.replace(offset, 0, lineEscape); + } catch (BadLocationException e) { + QtPlugin.log(e); + } + } + } else { + // Variable does not exist, create it + String baseVariable = variable + " += " + value + "\n"; //$NON-NLS-1$ //$NON-NLS-2$ + + // Check the contents of the document and re-format accordingly + if (document.get().trim().isEmpty()) { + try { + document.replace(0, document.getLength(), baseVariable); + } catch (BadLocationException e) { + QtPlugin.log(e); + } + } else if (document.get().endsWith("\n")) { //$NON-NLS-1$ + try { + document.replace(document.getLength(), 0, "\n" + baseVariable); //$NON-NLS-1$ + } catch (BadLocationException e) { + QtPlugin.log(e); + } + } else { + try { + document.replace(document.getLength(), 0, "\n\n" + baseVariable); //$NON-NLS-1$ + } catch (BadLocationException e) { + QtPlugin.log(e); + } + } + } + } + + /** + * Removes value from the specified variable and removes the previous line escape if necessary. The entire line is + * removed including any comments. If the value is not found, nothing happens. + * + * @param variable + * the name of the variable to remove from + * @param value + * the value to remove from the variable + */ + public void removeVariableValue(String variable, String value) { + QtProjectVariable var = parser.getVariable(variable); + + if (var != null) { + int line = var.getValueIndex(value); + if (line == 0 && var.getNumberOfLines() > 1) { + // Entering this block means we're removing the first line where more lines exist. + int offset = var.getValueOffsetForLine(line); + int end = var.getValueOffsetForLine(line + 1); + + try { + document.replace(offset, end - offset, ""); //$NON-NLS-1$ + } catch (BadLocationException e) { + QtPlugin.log(e); + } + } else if (line >= 0) { + int offset = var.getLineOffset(line); + int length = var.getLine(line).length(); + if (line > 0) { + // Remove the previous line feed character + offset--; + length++; + } + + try { + document.replace(offset, length, ""); //$NON-NLS-1$ + } catch (BadLocationException e) { + QtPlugin.log(e); + } + + // Remove the previous line's line escape character if necessary + if (line > 0 && line == var.getNumberOfLines() - 1) { + try { + offset = var.getLineEscapeOffset(line - 1); + length = var.getLineEscapeEnd(line - 1) - offset; + + document.replace(offset, length, ""); //$NON-NLS-1$ + } catch (BadLocationException e) { + QtPlugin.log(e); + } + } + } + } + } + + /** + * Get the IDocument currently being modified by this class. + * + * @return the document being modified + */ + public IDocument getDocument() { + return document; + } +} diff --git a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/pro/parser/QtProjectFileParser.java b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/pro/parser/QtProjectFileParser.java new file mode 100644 index 00000000000..3e9e3f075c5 --- /dev/null +++ b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/pro/parser/QtProjectFileParser.java @@ -0,0 +1,116 @@ +/******************************************************************************* + * Copyright (c) 2015 QNX Software Systems and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * QNX Software Systems - Initial API and implementation + *******************************************************************************/ +package org.eclipse.cdt.internal.qt.ui.pro.parser; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import java.util.concurrent.CopyOnWriteArrayList; + +import org.eclipse.jface.text.DocumentEvent; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IDocumentListener; + +/** + * Very basic parser for Qt Project Files that uses regular expressions. For now, this class only supports finding variables within + * a Document that follow the syntax: + * + *
+ * VARIABLE_NAME += value1 \ # comment
+ *     value2 \ # comment
+ *     value3
+ * 
+ * + * The assignment operator may be one of =, +=, -=, or *= in accordance with qmake syntax. Variable names are not checked for + * semantic validity. That is, this class does not make sure the variable name is a registered qmake variable, nor that there are + * multiple instances of a variable in the document. + */ +public class QtProjectFileParser implements IDocumentListener { + + IDocument document; + List variables; + + public QtProjectFileParser(IDocument doc) { + if (doc == null) { + throw new IllegalArgumentException("document cannot be null"); //$NON-NLS-1$ + } + + document = doc; + variables = parse(); + document.addDocumentListener(this); + } + + public IDocument getDocument() { + return document; + } + + private List parse() { + // Just build the list from scratch + List variables = new CopyOnWriteArrayList<>(); + try (Scanner scanner = new Scanner(document.get())) { + QtProjectVariable next; + while ((next = QtProjectVariable.findNextVariable(scanner)) != null) { + variables.add(next); + } + } + return variables; + } + + /** + * Retrieves a specific Qt Project Variable from the provided IDocument. If the variable cannot be found, + * null is returned instead. + *

+ * Note: This method is greedy in the sense that it returns the first match it finds. If multiple variables exist with + * the same name in the IDocument, this method will only return the first match. + *

+ * + * @param name + * the name of the variable + * @return the QtProjectVariable or null if it couldn't be found + */ + public QtProjectVariable getVariable(String name) { + for (QtProjectVariable v : variables) { + if (v.getName().equals(name)) { + return v; + } + } + return null; + } + + /** + * Returns a list of all Qt Project Variables found within the provided IDocument. A fresh list is always returned + * with the internal list copied into it. As such, modifying this list does not modify the internal list of the parser. + * + * @return the list of all Qt Project Variables + */ + public List getAllVariables() { + return new ArrayList<>(variables); + } + + @Override + public void documentAboutToBeChanged(DocumentEvent event) { + // Nothing to do + } + + @Override + public void documentChanged(DocumentEvent event) { + // Re-parse the document every time it changes + variables = parse(); + } + + @Override + protected void finalize() throws Throwable { + // Make sure that we are removed from the document's listeners + if (document != null) { + document.removeDocumentListener(this); + } + } +} diff --git a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/pro/parser/QtProjectVariable.java b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/pro/parser/QtProjectVariable.java new file mode 100644 index 00000000000..76d95919f0d --- /dev/null +++ b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/pro/parser/QtProjectVariable.java @@ -0,0 +1,389 @@ +/******************************************************************************* + * Copyright (c) 2015 QNX Software Systems and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * QNX Software Systems - Initial API and implementation + *******************************************************************************/ +package org.eclipse.cdt.internal.qt.ui.pro.parser; + +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; +import java.util.regex.MatchResult; +import java.util.regex.Pattern; + +/** + * Contains all information about a variable's representation in a Qt Project (.pro) File. This includes information about offsets, + * lengths, and textual representation of various components of a variable declaration such as its: + *
    + *
  • Name, such as "SOURCES"
  • + *
  • Assignment operator (= or +=)
  • + *
  • Values for a particular line
  • + *
  • Comments for a particular line
  • + *
  • Line feeds
  • + *
  • Line escapes (\)
  • + *
+ * Also contains the static method findNextVariable(Scanner) to perform the regular expressions lookup of the next + * variable in a document. + */ +public class QtProjectVariable { + private static final Pattern REGEX = Pattern.compile( + "(?m)^\\h*((?:[_a-zA-Z][_a-zA-Z0-9]*\\.)*[_a-zA-Z][_a-zA-Z0-9]*)\\h*(=|\\+=|-=|\\*=)\\h*([^#\\v]*?)\\h*((?:(\\\\)\\h*)?(#[^\\v]*)?$)"); //$NON-NLS-1$ + private static final Pattern LINE_ESCAPE_REGEX = Pattern.compile("(?m)^(\\h*)([^#\\v]*?)\\h*((?:(\\\\)\\h*)?(#[^\\v]*)?$)"); //$NON-NLS-1$ + + private static final int GROUP_VAR_NAME = 1; + private static final int GROUP_VAR_ASSIGNMENT = 2; + private static final int GROUP_VAR_CONTENTS = 3; + private static final int GROUP_VAR_TERMINATOR = 4; + private static final int GROUP_VAR_LINE_ESCAPE = 5; + private static final int GROUP_VAR_COMMENT = 6; + + private static final int GROUP_LINE_INDENT = 1; + private static final int GROUP_LINE_CONTENTS = 2; + private static final int GROUP_LINE_TERMINATOR = 3; + private static final int GROUP_LINE_LINE_ESCAPE = 4; + private static final int GROUP_LINE_COMMENT = 5; + + /** + * Finds the next Qt Project Variable within a String using the given Scanner. If there are no variables to be found, this + * method will return null. + * + * @param scanner + * the scanner to use for regular expressions matching + * @return the next variable or null if none + */ + public static QtProjectVariable findNextVariable(Scanner scanner) { + List matchResults = new ArrayList<>(); + + // Find the start of a variable declaration + String match = scanner.findWithinHorizon(REGEX, 0); + if (match == null) { + return null; + } + + // Get subsequent lines if the previous one ends with '\' + MatchResult matchResult = scanner.match(); + matchResults.add(matchResult); + if (matchResult.group(QtProjectVariable.GROUP_VAR_TERMINATOR).startsWith("\\")) { //$NON-NLS-1$ + do { + match = scanner.findWithinHorizon(LINE_ESCAPE_REGEX, 0); + if (match == null) { + // This means that we have a newline escape where another line doesn't exist + break; + } + + matchResult = scanner.match(); + matchResults.add(matchResult); + } while (matchResult.group(QtProjectVariable.GROUP_LINE_TERMINATOR).startsWith("\\")); //$NON-NLS-1$ + } + return new QtProjectVariable(matchResults); + } + + private final int startOffset; + private final int endOffset; + private final String text; + + private final List matchResults; + + /** + * Constructs a project file variable from a list of match results obtained from a Scanner. This constructor is + * only intended to be called from within the static method findNextVariable(Scanner). + * + * @param matches + * list of MatchResult + */ + private QtProjectVariable(List matches) { + this.startOffset = matches.get(0).start(); + this.endOffset = matches.get(matches.size() - 1).end(); + this.matchResults = matches; + + StringBuilder sb = new StringBuilder(); + for (MatchResult m : matches) { + sb.append(m.group()); + } + this.text = sb.toString(); + } + + /** + * Gets the offset of this variable relative to the start of its containing document. + * + * @return the offset of this variable + */ + public int getOffset() { + return startOffset; + } + + /** + * Gets the length of this variable as it appears in its containing document. + * + * @return the total length of this variable + */ + public int getLength() { + return endOffset - startOffset; + } + + /** + * Gets the name of this variable as it appears in the document. For example, the "SOURCES" variable. + * + * @return the name of this variable + */ + public String getName() { + return matchResults.get(0).group(GROUP_VAR_NAME); + } + + /** + * the assignment operator of this variable (+= or "=") + * + * @return the assignment operator + */ + public String getAssignmentOperator() { + return matchResults.get(0).group(GROUP_VAR_ASSIGNMENT); + } + + /** + * Returns a list of value(s) assigned to this variable. Each entry in the list represents a new line. + * + * @return a List containing all of the value(s) assigned to this variable + */ + public List getValues() { + List values = new ArrayList(); + values.add(matchResults.get(0).group(GROUP_VAR_CONTENTS)); + for (int i = 1; i < matchResults.size(); i++) { + values.add(matchResults.get(i).group(GROUP_LINE_CONTENTS)); + } + return values; + } + + /** + * Returns the indentation of the given line as a String. Mainly used by the QtProjectFileWriter to write back to the Document. + * + * @param line + * the line number to check + * @return a String representing the indentation of the given line + */ + public String getIndentString(int line) { + MatchResult match = matchResults.get(line); + if (line == 0) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < match.start(GROUP_VAR_CONTENTS) - match.start(); i++) { + sb.append(' '); + } + return sb.toString(); + } + return match.group(GROUP_LINE_INDENT); + } + + /** + * Retrieves the offset of the value portion of a given line relative to the start of its containing document. + * + * @param line + * the line to check + * @return the offset of the value + */ + public int getValueOffsetForLine(int line) { + if (line == 0) { + return matchResults.get(line).start(GROUP_VAR_CONTENTS); + } + return matchResults.get(line).start(GROUP_LINE_CONTENTS); + } + + /** + * Retrieves a String representing the value at a specific line of this variable. + * + * @param line + * the line to check + * @return the value + */ + public String getValueForLine(int line) { + if (line == 0) { + return matchResults.get(line).group(GROUP_VAR_CONTENTS); + } + return matchResults.get(line).group(GROUP_LINE_CONTENTS); + } + + /** + * Returns the ideal offset in the containing document at which a line escape can be inserted. + * + * @param line + * the line to check + * @return the ideal location for a line escape + */ + public int getLineEscapeReplacementOffset(int line) { + if (line == 0) { + return matchResults.get(line).end(GROUP_VAR_CONTENTS); + } + return matchResults.get(line).end(GROUP_LINE_CONTENTS); + } + + /** + * Returns the ideal String for the line escape character. This is mostly for spacing requirements and should be used in tandem + * with the method getLineEscapeReplacementOffset. + * + * @param line + * the line to check + * @return the ideal String for the line escape character + */ + public String getLineEscapeReplacementString(int line) { + int commentOffset = -1; + int contentsOffset = -1; + if (line == 0) { + commentOffset = matchResults.get(line).start(GROUP_VAR_COMMENT); + contentsOffset = matchResults.get(line).end(GROUP_VAR_CONTENTS); + } else { + commentOffset = matchResults.get(line).start(GROUP_LINE_COMMENT); + contentsOffset = matchResults.get(line).end(GROUP_LINE_CONTENTS); + } + + if (commentOffset > 0) { + if (commentOffset - contentsOffset == 0) { + return " \\ "; //$NON-NLS-1$ + } + } + return " \\"; //$NON-NLS-1$ + } + + /** + * Retrieves the offset of the line escape for a given line relative to its containing document. This method takes into account + * spacing and should be used to determine how to best remove a line escape character from a given line. + * + * @param line + * the line to check + * @return the offset of the line escape character + */ + public int getLineEscapeOffset(int line) { + if (line == 0) { + return matchResults.get(line).end(GROUP_VAR_CONTENTS); + } + return matchResults.get(line).end(GROUP_LINE_CONTENTS); + } + + /** + * Get the end position relative to the start of the containing document that contains the line escape character of the given + * line. This is used for removal of the line escape character and takes into account the spacing of the line. + * + * @param line + * the line to check + * @return the end position of the line escape character + */ + public int getLineEscapeEnd(int line) { + int end = -1; + if (line == 0) { + end = matchResults.get(line).end(GROUP_VAR_LINE_ESCAPE); + } else { + end = matchResults.get(line).end(GROUP_LINE_LINE_ESCAPE); + } + + if (end > 0) { + return end; + } + + if (line == 0) { + return matchResults.get(line).end(GROUP_VAR_TERMINATOR); + } + return matchResults.get(line).end(GROUP_LINE_TERMINATOR); + } + + /** + * Gets the end position of this variable relative to the containing document. + * + * @return the end position of this variable + */ + public int getEndOffset() { + return matchResults.get(matchResults.size() - 1).end(); + } + + /** + * Retrieves the full text of this variable as it appears in the document. + * + * @return the full String of this variable as it appears in the document + */ + public String getText() { + return text; + } + + /** + * Gets the total number of lines in this variable declaration. + * + * @return the total number of lines + */ + public int getNumberOfLines() { + return matchResults.size(); + } + + /** + * Retrieves a String representing the given line as it appears in the document. + * + * @param line + * the line to retrieve + * @return a String representing the line + */ + public String getLine(int line) { + return matchResults.get(line).group(); + } + + /** + * Retrieves the offset of the given line relative to its containing document. + * + * @param line + * the line to retrieve + * @return the line's offset in the document + */ + public int getLineOffset(int line) { + return matchResults.get(line).start(); + } + + /** + * Returns the line at which the specified value appears. This method checks the whole line for the value and will not match a + * subset of that String. This is equivalent to calling getValueIndex(value,false). + * + * @param value + * the value to search for + * @return the line that the value appears on or -1 if it doesn't exist + */ + public int getValueIndex(String value) { + return getValueIndex(value, false); + } + + /** + * Returns the line at which the specified value appears. This method checks the whole line for the value and will not match a + * subset of that String. If ignoreCase is false, this method searches for the value using + * equalsIgnoreCase instead of equals. + * + * @param value + * the value to search for + * @param ignoreCase + * whether or not the value is case-sensitive + * @return the line that the value appears on or -1 if it doesn't exist + */ + public int getValueIndex(String value, boolean ignoreCase) { + int line = 0; + for (String val : getValues()) { + if (ignoreCase) { + if (val.equalsIgnoreCase(value)) { + return line; + } + } else { + if (val.equals(value)) { + return line; + } + } + line++; + } + return -1; + } + + /** + * Gets the offset of the end of a given line relative to its containing document. + * + * @param line + * the line to check + * @return the offset of the end of the line + */ + public int getLineEnd(int line) { + return matchResults.get(line).end(); + } +} \ No newline at end of file diff --git a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/resources/QtProjectFileUpdateJob.java b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/resources/QtProjectFileUpdateJob.java new file mode 100644 index 00000000000..793bf4d8ae7 --- /dev/null +++ b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/resources/QtProjectFileUpdateJob.java @@ -0,0 +1,157 @@ +/******************************************************************************* + * Copyright (c) 2015 QNX Software Systems and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * QNX Software Systems - Initial API and implementation + *******************************************************************************/ +package org.eclipse.cdt.internal.qt.ui.resources; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.cdt.internal.qt.ui.QtUIPlugin; +import org.eclipse.cdt.internal.qt.ui.editor.QtProjectFileKeyword; +import org.eclipse.cdt.internal.qt.ui.pro.parser.QtProjectFileModifier; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; + +/** + * Job that calls the QtProjectFileModifier after changes to resources found in Qt Projects in order to update their + * SOURCES variable. + */ +public class QtProjectFileUpdateJob extends Job { + + private List deltaList; + + public QtProjectFileUpdateJob(List deltas) { + super("Update Qt Project File(s)"); //$NON-NLS-1$ + this.deltaList = deltas; + } + + private IFile findQtProjectFile(IProject project) throws CoreException { + for (IResource member : project.members()) { + if (member.getType() == IResource.FILE + && member.getFileExtension().equals("pro")) { //$NON-NLS-1$ + return (IFile) member; + } + } + return null; + } + + @Override + protected IStatus run(IProgressMonitor monitor) { + // Cache the project files so we don't continuously open them + Map modifierMap = new HashMap<>(); + Map projectFileMap = new HashMap<>(); + + for (IResourceDelta delta : deltaList) { + IResource resource = delta.getResource(); + IProject project = resource.getProject(); + QtProjectFileModifier modifier = modifierMap.get(project); + + if (modifier == null) { + IFile proFile = null; + try { + proFile = findQtProjectFile(project); + } catch (CoreException e) { + QtUIPlugin.log("Unable to find Qt Project File", e); //$NON-NLS-1$ + } + + // We can't update a project file if it doesn't exist + if (proFile == null) { + continue; + } + + // Cache the project file under its containing project and read its contents into a Document. + projectFileMap.put(project, proFile); + StringBuilder sb = new StringBuilder(); + try (InputStream is = proFile.getContents()) { + int read = -1; + while ((read = is.read()) > 0) { + sb.append((char) read); + } + IDocument document = new Document(sb.toString()); + modifier = new QtProjectFileModifier(document); + modifierMap.put(project, modifier); + } catch (IOException e) { + QtUIPlugin.log(e); + break; + } catch (CoreException e) { + QtUIPlugin.log(e); + break; + } + } + + // Determine from the file extension where we should add this resource + String variableKeyword = null; + if ("cpp".equals(resource.getFileExtension())) { //$NON-NLS-1$ + variableKeyword = QtProjectFileKeyword.VAR_SOURCES.getKeyword(); + } else if ("h".equals(resource.getFileExtension())) { //$NON-NLS-1$ + variableKeyword = QtProjectFileKeyword.VAR_HEADERS.getKeyword(); + } + + if ((delta.getFlags() & IResourceDelta.MOVED_FROM) > 0) { + // Resource was moved from another location. + if (project.getFullPath().isPrefixOf(delta.getMovedFromPath())) { + String oldValue = delta.getMovedFromPath().makeRelativeTo(project.getFullPath()).toString(); + String newValue = resource.getProjectRelativePath().toString(); + + if (modifier.replaceVariableValue(variableKeyword, oldValue, newValue)) { + // If we successfully replaced the variable, continue. If this line is not executed it means we failed to + // replace and the file will be added in the subsequent code for the ADDED case. + continue; + } + } + } else if ((delta.getFlags() & IResourceDelta.MOVED_TO) > 0) { + // Somewhat edge-case where a file from one Qt Project was moved to a different Qt Project. + if (project.getFullPath().isPrefixOf(delta.getMovedToPath())) { + // Getting here means that the replace was taken care of by the previous code. Otherwise, it will be removed in + // the subsequent code for the REMOVED case. + continue; + } + } + + if ((delta.getKind() & IResourceDelta.ADDED) > 0) { + String value = resource.getProjectRelativePath().toString(); + if (value != null) { + modifier.addVariableValue(variableKeyword, value); + } + } else if ((delta.getKind() & IResourceDelta.REMOVED) > 0) { + String value = resource.getProjectRelativePath().toString(); + if (value != null) { + modifier.removeVariableValue(variableKeyword, value); + } + } + } + + // Write all documents to their respective files + for (IProject project : projectFileMap.keySet()) { + IFile file = projectFileMap.get(project); + IDocument document = modifierMap.get(project).getDocument(); + + try { + file.setContents(new ByteArrayInputStream(document.get().getBytes()), 0, null); + } catch (CoreException e) { + QtUIPlugin.log(e); + } + } + return Status.OK_STATUS; + } +} diff --git a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/resources/QtResourceChangeListener.java b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/resources/QtResourceChangeListener.java new file mode 100644 index 00000000000..bfd679202c9 --- /dev/null +++ b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/resources/QtResourceChangeListener.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * Copyright (c) 2015 QNX Software Systems and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * QNX Software Systems - Initial API and implementation + *******************************************************************************/ +package org.eclipse.cdt.internal.qt.ui.resources; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.cdt.internal.qt.core.QtNature; +import org.eclipse.cdt.internal.qt.ui.QtUIPlugin; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceChangeEvent; +import org.eclipse.core.resources.IResourceChangeListener; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IResourceDeltaVisitor; +import org.eclipse.core.runtime.CoreException; + +/** + * Detects the addition or removal of a file to a Qt project. If one of these resource changes is found, it triggers an update of + * the project's *.pro file to reflect the change. + */ +public class QtResourceChangeListener implements IResourceChangeListener { + + @Override + public void resourceChanged(IResourceChangeEvent event) { + // No need to check for any events other than POST_CHANGE + if ((event.getType() & (IResourceChangeEvent.POST_CHANGE | IResourceChangeEvent.POST_BUILD)) == 0) { + return; + } + + final List deltaList = new ArrayList<>(); + IResourceDeltaVisitor visitor = new IResourceDeltaVisitor() { + + @Override + public boolean visit(IResourceDelta delta) { + IResource resource = delta.getResource(); + + if (resource.getType() == IResource.ROOT) { + // Always traverse children of the workspace root + return true; + } else if (resource.getType() == IResource.PROJECT) { + // Only traverse children of Qt Projects + try { + IProject project = (IProject) resource; + if (project.hasNature(QtNature.ID)) { + return true; + } + } catch (CoreException e) { + QtUIPlugin.log(e); + } + return false; + } else if (resource.getType() == IResource.FOLDER) { + // First, make sure this isn't the "build" folder + if (resource.getType() == IResource.FOLDER) { + if (resource.getName().equals("build")) { //$NON-NLS-1$ + return false; + } + } + + // Then check to make sure that the folder lies in a Qt Project + try { + IProject project = resource.getProject(); + if (project != null && project.hasNature(QtNature.ID)) { + return true; + } + } catch (CoreException e) { + QtUIPlugin.log(e); + } + return false; + } + + // We only care about added and removed resources at this point + if ((delta.getKind() & (IResourceDelta.ADDED | IResourceDelta.REMOVED)) == 0) { + return false; + } + + if ("cpp".equals(resource.getFileExtension()) //$NON-NLS-1$ + || "h".equals(resource.getFileExtension())) { //$NON-NLS-1$ + // If we make it to this point, then we have a .cpp or .h file that's been added to or removed from a Qt + // Project. Add it to the list of deltas so we can update the project file later. + deltaList.add(delta); + } + + // Doesn't really matter since this line can only be reached if we're dealing with a file that shouldn't have + // children anyway + return false; + } + }; + + try { + // Check all projects starting at the workspace root + event.getDelta().accept(visitor); + } catch (CoreException e) { + QtUIPlugin.log(e); + } + + // Schedule the job to update the .pro files + if (!deltaList.isEmpty()) { + new QtProjectFileUpdateJob(deltaList).schedule(); + } + } +} diff --git a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/resources/QtWorkspaceSaveParticipant.java b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/resources/QtWorkspaceSaveParticipant.java new file mode 100644 index 00000000000..726c064352c --- /dev/null +++ b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/resources/QtWorkspaceSaveParticipant.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright (c) 2015 QNX Software Systems and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * QNX Software Systems - Initial API and implementation + *******************************************************************************/ +package org.eclipse.cdt.internal.qt.ui.resources; + +import org.eclipse.core.resources.ISaveContext; +import org.eclipse.core.resources.ISaveParticipant; +import org.eclipse.core.runtime.CoreException; + +public class QtWorkspaceSaveParticipant implements ISaveParticipant { + + @Override + public void doneSaving(ISaveContext context) { + // Nothing to do + } + + @Override + public void prepareToSave(ISaveContext context) throws CoreException { + // Nothing to do + } + + @Override + public void rollback(ISaveContext context) { + // Nothing to do + } + + @Override + public void saving(ISaveContext context) throws CoreException { + context.needDelta(); + } + +} diff --git a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/text/ContentAssistProcessor.java b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/text/ContentAssistProcessor.java new file mode 100644 index 00000000000..b2f6c9ee7aa --- /dev/null +++ b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/text/ContentAssistProcessor.java @@ -0,0 +1,107 @@ +/******************************************************************************* + * Copyright (c) 2015 QNX Software Systems and others. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * QNX Software Systems - Initial API and implementation + *******************************************************************************/ +package org.eclipse.cdt.internal.qt.ui.text; + +import java.util.ArrayList; +import java.util.Locale; + +import org.eclipse.cdt.internal.qt.ui.QtUIPlugin; +import org.eclipse.cdt.internal.qt.ui.editor.QtProjectFileKeyword; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.contentassist.CompletionProposal; +import org.eclipse.jface.text.contentassist.ICompletionProposal; +import org.eclipse.jface.text.contentassist.IContentAssistProcessor; +import org.eclipse.jface.text.contentassist.IContextInformation; +import org.eclipse.jface.text.contentassist.IContextInformationValidator; + +public class ContentAssistProcessor implements IContentAssistProcessor { + private final IContextInformation[] NO_CONTEXTS = {}; + private final ICompletionProposal[] NO_COMPLETIONS = {}; + + @Override + public ICompletionProposal[] computeCompletionProposals(ITextViewer viewer, int offset) { + try { + IDocument document = viewer.getDocument(); + ArrayList result = new ArrayList<>(); + + // Search the list of keywords (case-insensitive) + String prefix = lastWord(document, offset).toLowerCase(Locale.ROOT); + for (QtProjectFileKeyword keyword : QtProjectFileKeyword.values()) { + if (prefix.isEmpty() || keyword.getKeyword().toLowerCase(Locale.ROOT).startsWith(prefix)) { + result.add(new CompletionProposal( + keyword.getKeyword(), + offset - prefix.length(), + prefix.length(), + keyword.getKeyword().length())); + } + } + return result.toArray(new ICompletionProposal[result.size()]); + } catch (Exception e) { + QtUIPlugin.log(e); + return NO_COMPLETIONS; + } + } + + /** + * Returns the valid Java identifier in a document immediately before the given offset. + * + * @param document + * the document + * @param offset + * the offset at which to start looking + * @return the Java identifier preceding this location or a blank string if none + */ + private String lastWord(IDocument document, int offset) { + try { + for (int n = offset - 1; n >= 0; n--) { + char c = document.getChar(n); + if (!Character.isJavaIdentifierPart(c)) { + return document.get(n + 1, offset - n - 1); + } + } + return document.get(0, offset); + } catch (BadLocationException e) { + QtUIPlugin.log(e); + } + return ""; //$NON-NLS-1$ + } + + @Override + public IContextInformation[] computeContextInformation(ITextViewer viewer, int offset) { + // No context information for now + return NO_CONTEXTS; + } + + @Override + public String getErrorMessage() { + return null; + } + + @Override + public IContextInformationValidator getContextInformationValidator() { + // No context information validator + return null; + } + + @Override + public char[] getCompletionProposalAutoActivationCharacters() { + // No auto activation + return null; + } + + @Override + public char[] getContextInformationAutoActivationCharacters() { + // No auto activation + return null; + } +} diff --git a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/text/QtProjectFileSourceViewerConfiguration.java b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/text/QtProjectFileSourceViewerConfiguration.java index e3371369c84..31bd4682c5d 100644 --- a/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/text/QtProjectFileSourceViewerConfiguration.java +++ b/qt/org.eclipse.cdt.qt.ui/src/org/eclipse/cdt/internal/qt/ui/text/QtProjectFileSourceViewerConfiguration.java @@ -14,6 +14,9 @@ import org.eclipse.cdt.internal.qt.ui.editor.QtProjectFileKeyword; import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocumentExtension3; import org.eclipse.jface.text.TextAttribute; +import org.eclipse.jface.text.contentassist.ContentAssistant; +import org.eclipse.jface.text.contentassist.IContentAssistProcessor; +import org.eclipse.jface.text.contentassist.IContentAssistant; import org.eclipse.jface.text.presentation.IPresentationReconciler; import org.eclipse.jface.text.presentation.PresentationReconciler; import org.eclipse.jface.text.rules.DefaultDamagerRepairer; @@ -89,4 +92,12 @@ public class QtProjectFileSourceViewerConfiguration extends TextSourceViewerConf return scanner; } + @Override + public IContentAssistant getContentAssistant(ISourceViewer sourceViewer) { + ContentAssistant contentAssistant = new ContentAssistant(); + IContentAssistProcessor processor = new ContentAssistProcessor(); + contentAssistant.setContentAssistProcessor(processor, IDocument.DEFAULT_CONTENT_TYPE); + contentAssistant.setInformationControlCreator(getInformationControlCreator(sourceViewer)); + return contentAssistant; + } }