1
0
Fork 0
mirror of https://github.com/eclipse-cdt/cdt synced 2025-07-23 17:05:26 +02:00

Bug 478938 - Automatically add source files to Qt Project File

Added basic content assist for built-in qmake variables since there are
so many of them.

Added a bunch of new classes dealing with basic parsing and modification
of Qt Project Files.  For now the parser is a simple regular expression
matcher that finds instances of variables.  The modification class
supports preservation of indentation and comments that are tied to
variables in the project file.

Change-Id: I0539458d5c1cf29e6c9c1246e4e717e7cbec1b84
Signed-off-by: Matthew Bastien <mbastien@blackberry.com>
This commit is contained in:
Matthew Bastien 2015-10-02 17:56:45 -04:00 committed by Gerrit Code Review @ Eclipse.org
parent 43da8ab7ac
commit 6c3f10576b
14 changed files with 1759 additions and 3 deletions

View file

@ -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());
}
}

View file

@ -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<QtProjectVariable> 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<QtProjectVariable> 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<QtProjectVariable> 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<QtProjectVariable> 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$
}
}

View file

@ -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);
}
}

View file

@ -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"

View file

@ -59,7 +59,7 @@
<editor
class="org.eclipse.cdt.internal.qt.ui.editor.QtProjectFileEditor"
default="true"
extensions="pro"
extensions="pro,pri"
id="org.eclipse.cdt.qt.ui.QtProjectFileEditor"
name="%qtProjectFileEditor.name">
</editor>

View file

@ -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

View file

@ -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:
*
* <pre>
* <code>SOURCES += file.cpp \ # This is the first line with value "file.cpp"
* file2.cpp # This is the second line with value "file2.cpp"</code>
* </pre>
*
* This class supports the following modifications to variables:
* <ul>
* <li><b>Add Value</b>: 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.</li>
* <li><b>Remove Value</b>: 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.</li>
* <li><b>Replace Value</b>: 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.</li>
* </ul>
* <p>
* 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.
* </p>
*/
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 <code>replaceVariableValue(variable,oldValue,newValue,true)</code> and will only match values that
* occupy an entire line within the variable declaration.
* <p>
* This method does <b>not</b> create a new value if the specified <code>oldValue</code> was not found. If this behavior is
* desired, then check for a return of <code>false</code> from this method and then call the <code>addVariableValue</code>
* method.
* </p>
* <p>
* <b>Note:</b> 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".
* </p>
*
* @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 <code>oldValue</code> with <code>newValue</code> if it is found within the given
* variable name. If <code>matchWholeLine</code> is false, this method will try to match sections of each line with the value of
* <code>oldValue</code>. If a match is found, only that portion of the line will be replaced. If <code>matchWholeLine</code> is
* true, this method will try to match the entire line with the value of <code>oldValue</code> and will replace that. All other
* line spacing and comments are preserved as only the value itself is replaced.
* <p>
* This method does <b>not</b> create a new value if <code>oldValue</code> was not found. If this behavior is desired, then
* check for a return of <code>false</code> from this method and then call the <code>addVariableValue</code> method.
* </p>
* <p>
* <b>Note:</b> 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".
* </p>
*
* @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 <code>value</code> 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 <code>value</code>.
*
* @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 <code>value</code> 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 <code>IDocument</code> currently being modified by this class.
*
* @return the document being modified
*/
public IDocument getDocument() {
return document;
}
}

View file

@ -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:
*
* <pre>
* <code>VARIABLE_NAME += value1 \ # comment
* value2 \ # comment
* value3</code>
* </pre>
*
* 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<QtProjectVariable> 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<QtProjectVariable> parse() {
// Just build the list from scratch
List<QtProjectVariable> 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 <code>IDocument</code>. If the variable cannot be found,
* <code>null</code> is returned instead.
* <p>
* <b>Note:</b> 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 <code>IDocument</code>, this method will only return the first match.
* </p>
*
* @param name
* the name of the variable
* @return the <code>QtProjectVariable</code> or <code>null</code> 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 <code>IDocument</code>. 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<QtProjectVariable> 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);
}
}
}

View file

@ -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:
* <ul>
* <li>Name, such as "SOURCES"</li>
* <li>Assignment operator (= or +=)</li>
* <li>Values for a particular line</li>
* <li>Comments for a particular line</li>
* <li>Line feeds</li>
* <li>Line escapes (\)</li>
* </ul>
* Also contains the static method <code>findNextVariable(Scanner)</code> 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 <code>null</code>.
*
* @param scanner
* the scanner to use for regular expressions matching
* @return the next variable or <code>null</code> if none
*/
public static QtProjectVariable findNextVariable(Scanner scanner) {
List<MatchResult> 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<MatchResult> matchResults;
/**
* Constructs a project file variable from a list of match results obtained from a <code>Scanner</code>. This constructor is
* only intended to be called from within the static method <code>findNextVariable(Scanner)</code>.
*
* @param matches
* list of <code>MatchResult</code>
*/
private QtProjectVariable(List<MatchResult> 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 <code>"SOURCES"</code> variable.
*
* @return the name of this variable
*/
public String getName() {
return matchResults.get(0).group(GROUP_VAR_NAME);
}
/**
* the assignment operator of this variable (<code>+=</code> or <code>"="</code>)
*
* @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<String> getValues() {
List<String> values = new ArrayList<String>();
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 <code>String</code> 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 <code>getLineEscapeReplacementOffset</code>.
*
* @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 <code>getValueIndex(value,false)</code>.
*
* @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 <code>ignoreCase</code> is <code>false</code>, this method searches for the value using
* <code>equalsIgnoreCase</code> instead of <code>equals</code>.
*
* @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();
}
}

View file

@ -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 <code>QtProjectFileModifier</code> after changes to resources found in Qt Projects in order to update their
* <code>SOURCES</code> variable.
*/
public class QtProjectFileUpdateJob extends Job {
private List<IResourceDelta> deltaList;
public QtProjectFileUpdateJob(List<IResourceDelta> 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<IProject, QtProjectFileModifier> modifierMap = new HashMap<>();
Map<IProject, IFile> 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;
}
}

View file

@ -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<IResourceDelta> 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();
}
}
}

View file

@ -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();
}
}

View file

@ -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<ICompletionProposal> 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;
}
}

View file

@ -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;
}
}