diff --git a/ccw.core.test/META-INF/MANIFEST.MF b/ccw.core.test/META-INF/MANIFEST.MF
index fdac11d3..8b584e0f 100644
--- a/ccw.core.test/META-INF/MANIFEST.MF
+++ b/ccw.core.test/META-INF/MANIFEST.MF
@@ -11,5 +11,6 @@ Require-Bundle: org.eclipse.swtbot.eclipse.finder;bundle-version="2.3.0",
org.eclipse.swtbot.junit4_x;bundle-version="2.3.0",
org.hamcrest.core;bundle-version="1.3",
org.hamcrest.library;bundle-version="1.3",
+ org.eclipse.jdt.annotation;bundle-version="[1.1.0,2.0.0)";resolution:=optional,
org.apache.log4j
Eclipse-RegisterBuddy: org.apache.log4j
diff --git a/ccw.core.test/src/clj/ccw/editors/clojure/folding_support_test.clj b/ccw.core.test/src/clj/ccw/editors/clojure/folding_support_test.clj
new file mode 100644
index 00000000..f0b9387f
--- /dev/null
+++ b/ccw.core.test/src/clj/ccw/editors/clojure/folding_support_test.clj
@@ -0,0 +1,186 @@
+;*******************************************************************************
+;* Copyright (c) 2015 Laurent PETIT.
+;* 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:
+;* Andrea Richiardi - initial implementation (code reviewed by Laurent Petit)
+;*******************************************************************************/
+(ns ^{:author "Andrea Richiardi"}
+ ccw.editors.clojure.folding-support-test
+ (:require [clojure.test :refer :all]
+ [clojure.pprint :as pp :refer [pprint]]
+ [clojure.zip :as zip]
+ [clojure.edn :as edn :refer [read-string]]
+ [ccw.test-common :refer :all]
+ [paredit.core :as p]
+ [paredit.loc-utils :as lu]
+ [ccw.editors.clojure.folding-support :refer :all]
+ [ccw.editors.clojure.editor-support :as es])
+ (:import org.eclipse.jface.text.Position
+ ccw.editors.clojure.folding.FoldingDescriptor
+ ccw.preferences.PreferenceInitializer))
+
+(def init-small-state #(es/init-text-buffer nil "(ns org.small.namespace)"))
+(def init-medium-state #(es/init-text-buffer nil "(ns org.medium.namespace) (defn myfun \"Example\" [] (let [bind1 \"Ciao\" bind2 [\\b \\u \\d \\d \\y]] bind3 #{:a 1 :b 2 :c 3} (println bind1 bind2)))"))
+(def init-empty-state #(es/init-text-buffer nil ""))
+
+(def init-nested-list-state #(es/init-text-buffer nil "(filter even? (range 0 100))"))
+(def init-single-line-state #(es/init-text-buffer nil "(ns ^{:doc \"test ns\"} ns.core)"))
+(def init-multi-line-state #(es/init-text-buffer nil "(defn my-fun
+ \"Lorem ipsum dolor sit amet,
+ consectetur adipiscing elit,
+ sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\"
+ (println \"docet\"))
+ "))
+
+(with-private-vars [ccw.editors.clojure.folding-support [any-enabled?
+ enabled-tags
+ pos->vec
+ locs-with-tags
+ loc->position
+ inc-token-occ
+ dec-token-occ
+ update-token-map
+ son-of-a-multi-line-loc?
+ next-is-newline?
+ parse-locs
+ folding-positions
+ from-java
+ to-java]]
+ (deftest folding-support-tests
+
+ (testing "testing java interop ..."
+ (let [descriptors [{:id :fold-parens :enabled true :loc-tags #{:list}} {:id :fold-double-apices :enabled true :loc-tags #{:string}} {:id :fold-braces :enabled true :loc-tags #{:map :set}}]
+ java-descriptors (map #(to-java FoldingDescriptor %) descriptors)
+ tripped-descriptors (map #(from-java %) java-descriptors)]
+ (is (= (:id descriptors) (:id tripped-descriptors)))
+ (is (= (:enabled descriptors) (:enabled tripped-descriptors)))
+ (is (= (:loc-tags descriptors) (:loc-tags tripped-descriptors))))
+ (let [descriptors (edn/read-string PreferenceInitializer/DEFAULT_FOLDING_DESCRIPTORS)
+ java-descriptors (map #(to-java FoldingDescriptor %) descriptors)
+ tripped-descriptors (map #(from-java %) java-descriptors)]
+ (is (not (empty? java-descriptors)) "Converted descriptors should not be empty")
+ (is (every? #(instance? FoldingDescriptor %) java-descriptors) "Converted descriptors should all be instances of FoldingDescriptor")
+ (is (= descriptors tripped-descriptors) "Round trip from/to java for the default preference descriptors")))
+
+ (testing "testing update-token-map and inc/dec-token-occ..."
+ (let [parse-tree (:parse-tree @(init-medium-state))
+ root-loc (lu/parsed-root-loc parse-tree)
+ loc-seq (locs-with-tags root-loc #{:list :vector})
+ list-loc1 (first (take-while #(= :list (lu/loc-tag %1)) loc-seq))
+ list-loc2 (second (take-while #(= :list (lu/loc-tag %1)) loc-seq))
+ vector-loc (take-while #(= :vector (lu/loc-tag %1)) loc-seq)
+ initial-map (update-token-map {} "(" list-loc1)
+ list-vector-map (update-token-map initial-map "[" vector-loc)
+ list2-vector-map (update-token-map list-vector-map "(" list-loc2)]
+ (is (= {"(" {:occurrences 1 :loc list-loc1}} initial-map) "Initialization should conj to map")
+ (is (= {"(" {:occurrences 1 :loc list-loc1}, "[" {:occurrences 1 :loc vector-loc}}
+ list-vector-map) "Updating a map with different token should add an entry")
+ (is (= {"(" {:occurrences 2 :loc list-loc1}, "[" {:occurrences 1 :loc vector-loc}}
+ list2-vector-map) "Updating a map with a token already preset should increase :occurrences")
+ (is (not (= {"(" {:occurrences 2 :loc list-loc2}, "[" {:occurrences 1 :loc vector-loc}}
+ list2-vector-map)) "Updating a map with a token already set should not changing the initial loc")
+ (is (= {"(" {:occurrences 2 :loc list-loc1}, "[" {:occurrences 1 :loc vector-loc}}
+ (inc-token-occ list-vector-map "(")) "Should correctly increment (")
+ (is (= {"(" {:occurrences 1 :loc list-loc1}, "[" {:occurrences 0 :loc vector-loc}}
+ (dec-token-occ list-vector-map "[")) "Should correctly decrement [")))
+
+ (testing "testing locs-with-tags..."
+ (let [parse-tree (:parse-tree @(init-medium-state))
+ root-loc (lu/parsed-root-loc parse-tree)]
+ (is (every? #(= (lu/loc-tag %1) :vector) (locs-with-tags root-loc #{:vector})))
+ (is (every? #(= (lu/loc-tag %1) :list) (locs-with-tags root-loc #{:list})))
+ (is (every? #(get #{:vector :string-body} (lu/loc-tag %1)) (locs-with-tags root-loc #{:vector :string-body})))
+ (is (nil? (locs-with-tags root-loc #{:not-there})) "Should return a nil if empty")))
+
+ (testing "testing son-of-a-multi-line-loc? ..."
+ (let [parse-tree (:parse-tree @(init-single-line-state))
+ root-loc (lu/parsed-root-loc parse-tree)
+ loc-seq (locs-with-tags root-loc #{:list :string})]
+ ;; Refer to the samples above to visually see the test case(s)
+ ;; single line loc should stay, as the word says, on a unique line with no "\n"
+ (is (= [false false false false] (map son-of-a-multi-line-loc? loc-seq))) "When locs are on a single line, it should always return true")
+ (let [parse-tree (:parse-tree @(init-multi-line-state))
+ root-loc (lu/parsed-root-loc parse-tree)
+ loc-seq (locs-with-tags root-loc #{:list :string})]
+ ;; Refer to the samples above to visually see the test case(s)
+ (is (= [true true true false false false false true] (map son-of-a-multi-line-loc? loc-seq))) "Should correctly report when loc enclose in a multi-line form"))
+
+ ;; (testing "testing enclose?"
+ ;; (is (enclose? (Position. 2 40) (Position. 3 30)) "At non boundaries enclose? returns true [1]")
+ ;; (is (not (enclose? (Position. 2 40) (Position. 2 30))) "At boundaries enclose? returns false [1]")
+ ;; (is (enclose? (Position. 4 10) (Position. 5 8)) "At non boundaries enclose? returns true [2]")
+ ;; (is (not (enclose? (Position. 4 10) (Position. 5 9))) "At boundaries enclose? returns false [2]")
+ ;; )
+
+ ;; (testing "testing overlap?"
+ ;; (is (not (overlap? [])) "false on empty seqs")
+ ;; (is (not (overlap? [(Position. 2 40)])) "false on one-element seqs")
+ ;; (is (not (overlap? [(Position. 1 3) (Position. 4 10) (Position. 15 8)])) "Positions should not overlap")
+ ;; (is (not (overlap? [(Position. 2 40) (Position. 42 10)])) "Pos1's offset+length equal to pos2's offset does not overlap")
+ ;; (is (overlap? [(Position. 2 40) (Position. 41 10)]) "Pos1's offset+length less than pos2's offset does overlap")
+ ;; (is (overlap? [(Position. 2 40) (Position. 1 30) (Position. 4 10) (Position. 5 8)]) "Every position should not overlap with each other [second is wrong]"))
+
+ (testing "testing next-is-newline? ..."
+ (let [parse-tree (:parse-tree @(init-multi-line-state))
+ root-loc (lu/parsed-root-loc parse-tree)
+ loc-seq (locs-with-tags root-loc #{:list :string})]
+ ;; Refer to the samples above to visually see the test case(s)
+ (is (= [false false true false false false false true] (map next-is-newline? loc-seq ))) "Should correctly report when loc is newline"))
+
+ (testing "testing parse-locs..."
+ (let [parse-tree (:parse-tree @(init-small-state))
+ root-loc (lu/parsed-root-loc parse-tree)
+ loc-seq (locs-with-tags root-loc #{:list})
+ position-set (trampoline parse-locs loc-seq {} #{})]
+ (is (set? position-set) "A set of org.eclipse.jface.text.Position should be returned")
+ (is (= [[1 23]] (map pos->vec position-set))) "Folding at the right place")
+ (let [parse-tree (:parse-tree @(init-nested-list-state))
+ root-loc (lu/parsed-root-loc parse-tree)
+ loc-seq (locs-with-tags root-loc #{:list})]
+ (is (= [[1 27]] (map pos->vec (trampoline parse-locs loc-seq {} #{})))) "Only top-level list is folded"))
+
+ (testing "testing any-enabled? ..."
+ (let [all-disabled-descriptors [{:id :fold-parens :enabled false :loc-tags #{:list}} {:id :fold-double-apices :enabled false :loc-tags #{:string}}]
+ first-disabled-descriptors [{:id :fold-parens :enabled false :loc-tags #{:list}} {:id :fold-double-apices :enabled true :loc-tags #{:string}}]
+ second-disabled-descriptors [{:id :fold-parens :enabled true :loc-tags #{:list}} {:id :fold-double-apices :enabled false :loc-tags #{:string}}]
+ all-enabled-descriptors [{:id :fold-parens :enabled true :loc-tags #{:list}} {:id :fold-double-apices :enabled true :loc-tags #{:string}}]]
+ (is (not (any-enabled? all-disabled-descriptors)) "If all descriptors are disabled return false")
+ (is (any-enabled? first-disabled-descriptors) "If second descriptor is enabled return true (at least one must be enabled)")
+ (is (any-enabled? second-disabled-descriptors) "If first descriptor is enabled return true (at least one must be enabled)")
+ (is (any-enabled? all-enabled-descriptors) "If all descriptors are enabled return true")))
+
+ (testing "testing enabled-tags ..."
+ (let [all-disabled-descs [{:id :fold-parens :enabled false :loc-tags #{:list}} {:id :fold-double-apices :enabled false :loc-tags #{:string}}]
+ string-enabled-descs [{:id :fold-parens :enabled false :loc-tags #{:list}} {:id :fold-double-apices :enabled true :loc-tags #{:string}}]
+ list-enabled-descs [{:id :fold-parens :enabled true :loc-tags #{:list}} {:id :fold-double-apices :enabled false :loc-tags #{:string}}]
+ all-enabled-descs [{:id :fold-parens :enabled true :loc-tags #{:list}} {:id :fold-double-apices :enabled true :loc-tags #{:string}}]
+ multiple-tags-descs [{:id :fold-parens :enabled true :loc-tags #{:list}} {:id :fold-double-apices :enabled true :loc-tags #{:string}} {:id :fold-braces :enabled true :loc-tags #{:map :set}}]]
+ (is (= #{} (enabled-tags all-disabled-descs)) "If all descriptors are disabled return empty set")
+ (is (= #{:string} (enabled-tags string-enabled-descs)) "If string descriptor is enabled return #{:string}")
+ (is (= #{:list} (enabled-tags list-enabled-descs)) "If list descriptor is enabled return #{:list}")
+ (is (= #{:list :string} (enabled-tags all-enabled-descs)) "If all enabled return #{:list :string}")
+ (is (= #{:list :string :map :set} (enabled-tags multiple-tags-descs)) "With multiple tags, all enabled, return #{:list :string :map :set}")))
+
+ (testing "testing folding-positions..."
+ (let [descriptors [{:id :fold-parens :enabled false :loc-tags #{:list}} {:id :fold-double-apices :enabled false :loc-tags #{:string}}]
+ position-set (folding-positions @(init-small-state) descriptors)]
+ (is (empty? position-set)) "if no descriptor is enabled folding should be empty")
+ (let [descriptors [{:id :fold-parens :enabled true :loc-tags #{:list}} {:id :fold-double-apices :enabled true :loc-tags #{:string}}]
+ position-set (folding-positions @(init-small-state) descriptors)]
+ (is (set? position-set) "A set of org.eclipse.jface.text.Position should be returned")
+ (is (empty? position-set)) "Small state folding should be empty")
+ (let [descriptors [{:id :fold-parens :enabled true :loc-tags #{:list}} {:id :fold-double-apices :enabled true :loc-tags #{:string}}]
+ position-set (folding-positions @(init-single-line-state) descriptors)]
+ (is (empty? position-set)) "Single-line state folding should be empty")
+ (let [all-descriptors [{:id :fold-parens :enabled true :loc-tags #{:list}} {:id :fold-double-apices :enabled true :loc-tags #{:string}}]
+ list-descriptors [{:id :fold-parens :enabled true :loc-tags #{:list}} {:id :fold-double-apices :enabled false :loc-tags #{:string}}]
+ string-descriptors [{:id :fold-parens :enabled false :loc-tags #{:list}} {:id :fold-double-apices :enabled true :loc-tags #{:string}}]]
+ (is (some #{[1 160] [15 126]} (map pos->vec (folding-positions @(init-multi-line-state) all-descriptors))) "Multi-line state (:list and :string enabled) should return only two positions")
+ (is (some #{[1 160]} (map pos->vec (folding-positions @(init-multi-line-state) list-descriptors))) "Multi-line state (:list enabled) should return only the first position")
+ (is (some #{[15 126]} (map pos->vec (folding-positions @(init-multi-line-state) string-descriptors))) "Multi-line state (:string enabled) should return only the second position")))))
+
+;; (run-tests)
diff --git a/ccw.core.test/src/java/ccw/core/BotUtils.java b/ccw.core.test/src/java/ccw/core/BotUtils.java
index df0c0345..668ec823 100644
--- a/ccw.core.test/src/java/ccw/core/BotUtils.java
+++ b/ccw.core.test/src/java/ccw/core/BotUtils.java
@@ -21,11 +21,14 @@
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
+import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.widgets.Widget;
import org.eclipse.swtbot.eclipse.finder.SWTWorkbenchBot;
+import org.eclipse.swtbot.eclipse.finder.widgets.SWTBotEditor;
import org.eclipse.swtbot.eclipse.finder.widgets.SWTBotView;
import org.eclipse.swtbot.swt.finder.SWTBot;
+import org.eclipse.swtbot.swt.finder.SWTBotAssert;
import org.eclipse.swtbot.swt.finder.exceptions.WidgetNotFoundException;
import org.eclipse.swtbot.swt.finder.matchers.WidgetMatcherFactory;
import org.eclipse.swtbot.swt.finder.waits.Conditions;
@@ -37,8 +40,13 @@
import org.eclipse.swtbot.swt.finder.widgets.SWTBotTree;
import org.eclipse.swtbot.swt.finder.widgets.SWTBotTreeItem;
import org.eclipse.swtbot.swt.finder.widgets.TimeoutException;
+import org.eclipse.ui.IEditorPart;
import org.hamcrest.Matcher;
+import waits.ResourceInSync;
+import ccw.core.bots.FoldingBot;
+import ccw.editors.clojure.ClojureEditor;
+
/**
* Wrapper utility class for common Counterclockwise UI tests.
*
@@ -221,11 +229,7 @@ public BotUtils waitForProject(String projName) {
}
public BotUtils waitForResource(IResource resource) {
- boolean isSync = false;
- while (isSync) {
- bot.sleep(500);
- isSync = resource.isSynchronized(IResource.DEPTH_INFINITE);
- }
+ bot.waitUntil(new ResourceInSync(resource));
return this;
}
@@ -280,12 +284,34 @@ public BotUtils assertProjectExists(String projectName) {
return this;
}
- public BotUtils createAndWaitForProject(String projectName) {
- return createClojureProject(projectName)
+ public BotUtils createAndWaitForProjectIfNecessary(String projectName) {
+ SWTBotTree packageExplorerTree = bot.viewByTitle("Package Explorer").bot().tree();
+ boolean found = false;
+
+ try {
+ packageExplorerTree.getTreeItem(projectName);
+ found = true;
+ } catch (WidgetNotFoundException e) {
+ Logger.getLogger(this.getClass()).debug("Caught and handled exception: " + e.getMessage());
+ } catch (SWTException e) {
+ Logger.getLogger(this.getClass()).debug("Caught and handled exception: " + e.getMessage());
+ } catch (TimeoutException e) {
+ Logger.getLogger(this.getClass()).debug("Caught and handled exception: " + e.getMessage());
+ }
+
+ if (!found) {
+ return createAndWaitForProject(projectName);
+ } else {
+ return this;
+ }
+ }
+
+ public BotUtils createAndWaitForProject(String projectName) {
+ return createClojureProject(projectName)
.waitForWorkspace()
.quietlySendUpdateDependenciesToBackground()
.waitForProject(projectName);
- }
+ }
public BotUtils sendToBackground(Matcher matcher, long timeout, long delay) {
bot.waitUntil(Conditions.waitForWidget(matcher), timeout, delay);
@@ -322,8 +348,8 @@ public BotUtils quietlySendUpdateDependenciesToBackground() {
return this;
}
- public BotUtils selectInClojureMenu(String entryLabel) throws Exception {
- menu("Clojure", entryLabel).click();
+ public BotUtils selectInClojureMenu(String label) throws Exception {
+ menu("Clojure", label).click();
return this;
}
@@ -347,7 +373,7 @@ public BotUtils closeRepl() throws Exception {
}
public BotUtils purgeProject(String projectName) {
- return deleteProject(projectName).deletingOnDisk().OK().quietlyContinuingIfNotInSync();
+ return deleteProject(projectName).deletingOnDisk().ok().quietlyContinuingIfNotInSync();
}
public BotUtils deleteProject(String projectName) {
@@ -383,7 +409,7 @@ public BotUtils quietlyContinuingIfNotInSync() {
return this;
}
- public BotUtils OK() {
+ public BotUtils ok() {
bot.button("OK").click();
return this;
}
@@ -396,4 +422,116 @@ public BotUtils cancel() {
public SWTWorkbenchBot bot() {
return bot;
}
+
+ public class Editor {
+ public final SWTBotEditor active;
+ public final ClojureEditor clojure;
+ public Editor(SWTBotEditor sbe, ClojureEditor ce) {
+ this.active = sbe;
+ this.clojure = ce;
+ }
+ }
+
+ /**
+ * Return the pair Editor
+ * @return The pair or null;
+ */
+ public @Nullable Editor editor() {
+ SWTBotEditor activeEditor = bot.activeEditor();
+ if (activeEditor != null) {
+ IEditorPart part = activeEditor.getReference().getEditor(true);
+ if (part != null) {
+ return new Editor(activeEditor, (ClojureEditor) part.getAdapter(ClojureEditor.class));
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Replace the whole text in the current active editor with the input.
+ * @param text The new text.
+ * @return
+ */
+ public BotUtils replaceTextOnActiveEditor(final String text) {
+ editor().active.toTextEditor().getStyledText().setText(text);
+ return this;
+ }
+
+ public BotUtils saveActiveEditor() {
+ editor().active.save();
+ return this;
+ }
+
+ public BotUtils focusActiveEditor() {
+ editor().active.setFocus();
+ return this;
+ }
+
+ public BotUtils closeActiveEditor() {
+ editor().active.close();
+ return this;
+ }
+
+ /**
+ * Clear and save the current active editor.
+ * @return
+ */
+ public BotUtils clsActiveEditor() {
+ replaceTextOnActiveEditor("");
+ saveActiveEditor();
+ return this;
+ }
+
+ public BotUtils openWindowPreferences() {
+ menu(MenuLabels.WINDOW, MenuLabels.PREFERENCES).click();
+ return this;
+ }
+
+ public BotUtils expandTreeItem(SWTBotTreeItem item) {
+ if (!item.isExpanded()) {
+ item.expand();
+ }
+ return this;
+ }
+
+ /**
+ * Select a page in Window -> Preferences given the name(s).
+ * @param pageName The page name.
+ * @param subPageNames The names of the sub pages.
+ * @return
+ */
+ public BotUtils selectPreferencePage(String pageName, String... subPageNames) {
+ openWindowPreferences();
+ SWTBotShell preferences = bot().shell(PrefStrings.TITLE);
+
+ SWTBotTreeItem pageItem = preferences.bot().tree().getTreeItem(pageName);
+ expandTreeItem(pageItem);
+
+ SWTBotTreeItem subPageItem = pageItem;
+ for (String name : subPageNames) {
+ subPageItem = subPageItem.getNode(name);
+ expandTreeItem(subPageItem);
+ }
+ subPageItem.select();
+ return this;
+ }
+
+ public BotUtils replaceTextInFile(String projectName, String fileName, String text) throws Exception {
+ doubleClickOnFileInProject(projectName, fileName).replaceTextOnActiveEditor(text).saveActiveEditor();
+ return this;
+ }
+
+ public BotUtils assertPreferencePage(String pageName) {
+ SWTBotAssert.assertText(pageName, bot().clabel());
+ return this;
+ }
+
+ /**
+ * Return the FoldingBot
+ * @return
+ * @throws Exception
+ */
+ public static FoldingBot foldingBot() throws Exception {
+ return new FoldingBot(new BotUtils());
+ }
}
diff --git a/ccw.core.test/src/java/ccw/core/ClojureEditorTests.java b/ccw.core.test/src/java/ccw/core/ClojureEditorTests.java
index 2a0b323e..d48b1630 100644
--- a/ccw.core.test/src/java/ccw/core/ClojureEditorTests.java
+++ b/ccw.core.test/src/java/ccw/core/ClojureEditorTests.java
@@ -28,7 +28,7 @@ public class ClojureEditorTests {
public static BotUtils bot = null;
public static final String PROJECT_NAME = "editor-test";
public static final String CORE_CLJ_NAME = "src/editor_test/core.clj";
-
+
@BeforeClass
public static void setupClass() throws Exception {
bot = new BotUtils();
diff --git a/ccw.core.test/src/java/ccw/core/ClojureTests.java b/ccw.core.test/src/java/ccw/core/ClojureTests.java
index c9328794..5d491e80 100644
--- a/ccw.core.test/src/java/ccw/core/ClojureTests.java
+++ b/ccw.core.test/src/java/ccw/core/ClojureTests.java
@@ -34,6 +34,7 @@ public ClojureTests() {
namespaces.add("ccw.extensions-test");
namespaces.add("ccw.util-test");
namespaces.add("ccw.editors.clojure.hover-support-test");
+ namespaces.add("ccw.editors.clojure.folding-support-test");
requireNamespaces(namespaces);
}
diff --git a/ccw.core.test/src/java/ccw/core/FoldingTests.java b/ccw.core.test/src/java/ccw/core/FoldingTests.java
new file mode 100644
index 00000000..19a3ac7a
--- /dev/null
+++ b/ccw.core.test/src/java/ccw/core/FoldingTests.java
@@ -0,0 +1,160 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Laurent Petit.
+ * 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:
+ * Andrea RICHIARDI - initial implementation
+ *******************************************************************************/
+package ccw.core;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+
+import java.util.Map;
+
+import org.eclipse.jface.text.Position;
+import org.eclipse.jface.text.source.Annotation;
+import org.eclipse.swtbot.swt.finder.junit.SWTBotJunit4ClassRunner;
+import org.eclipse.swtbot.swt.finder.widgets.TimeoutException;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import ccw.core.bots.FoldingBot;
+import ccw.util.TestUtil;
+
+@RunWith(SWTBotJunit4ClassRunner.class)
+public class FoldingTests {
+
+ public static final String MULTI_LINE_FORM = "(defn my-fun\n\"Lorem ipsum dolor sit amet,\nconsectetur adipiscing elit,\nsed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\"\n(println \"docet\"))";
+ public static final Position MULTI_LINE_LIST_ANNOTATION_POSITION = new Position(1, 156);
+ public static final Position MULTI_LINE_STRING_ANNOTATION_POSITION = new Position(14, 124);
+
+ public static final String PROJECT_NAME = "editor-test";
+ public static final String CORE_CLJ_NAME = "src/editor_test/core.clj";
+
+ public static FoldingBot bot = null;
+
+ @BeforeClass
+ public static void setupClass() throws Exception {
+ bot = BotUtils.foldingBot();
+ }
+
+ @AfterClass
+ public static void cleanClass() throws Exception {
+ bot.utils.purgeProject(PROJECT_NAME);
+ }
+
+ @Before
+ public void initTest() throws Exception {
+ bot.utils.openJavaPerspective().createAndWaitForProjectIfNecessary(PROJECT_NAME);
+ }
+
+ //////////////////////
+ /// TT EE SS TT SS ///
+ //////////////////////
+
+ @Test
+ public void shouldSelectFoldingPreferencePage() throws Exception {
+ bot.selectPreferencePage().utils
+ .assertPreferencePage(PrefStrings.TREE_ENTRY_CLOJURE_EDITOR_FOLDING)
+ .ok();
+ }
+
+ @Test
+ public void canFoldParensAndDoubleApices() throws Exception {
+ bot.selectPreferencePage()
+ .checkPreference(FoldingBot.TABLE_FOLD_TARGET_PAREN)
+ .checkPreference(FoldingBot.TABLE_FOLD_TARGET_DOUBLEAPICES)
+ .ok()
+ .enableProjectionPreference()
+ .fillAndWaitForAnnotations(PROJECT_NAME, CORE_CLJ_NAME, MULTI_LINE_FORM);
+
+ Map projectionMap = TestUtil.getProjectionMap(bot.utils.editor().clojure);
+
+ assertThat("Editor should contain correct folding positions",
+ projectionMap.values(),
+ containsInAnyOrder(MULTI_LINE_LIST_ANNOTATION_POSITION,
+ MULTI_LINE_STRING_ANNOTATION_POSITION));
+
+ bot.utils.clsActiveEditor().closeActiveEditor();
+ }
+
+ @Test
+ public void canFoldParensOnly() throws Exception {
+ bot.selectPreferencePage()
+ .checkPreference(FoldingBot.TABLE_FOLD_TARGET_PAREN)
+ .uncheckPreference(FoldingBot.TABLE_FOLD_TARGET_DOUBLEAPICES)
+ .ok()
+ .enableProjectionPreference()
+ .fillAndWaitForAnnotations(PROJECT_NAME, CORE_CLJ_NAME, MULTI_LINE_FORM);
+
+ Map projectionMap = TestUtil.getProjectionMap(bot.utils.editor().clojure);
+
+ assertThat("Editor should contain only parens (top level lists) folding positions",
+ projectionMap.values(),
+ contains(MULTI_LINE_LIST_ANNOTATION_POSITION));
+
+ bot.utils.clsActiveEditor().closeActiveEditor();
+ }
+
+ @Test
+ public void canFoldDoubleApicesOnly() throws Exception {
+ bot.selectPreferencePage()
+ .uncheckPreference(FoldingBot.TABLE_FOLD_TARGET_PAREN)
+ .checkPreference(FoldingBot.TABLE_FOLD_TARGET_DOUBLEAPICES)
+ .ok()
+ .enableProjectionPreference()
+ .fillAndWaitForAnnotations(PROJECT_NAME, CORE_CLJ_NAME, MULTI_LINE_FORM);
+
+ Map projectionMap = TestUtil.getProjectionMap(bot.utils.editor().clojure);
+
+ assertThat("Editor should contain only string folding positions",
+ projectionMap.values(),
+ contains(MULTI_LINE_STRING_ANNOTATION_POSITION));
+
+ bot.utils.clsActiveEditor().closeActiveEditor();
+ }
+
+ @Test(expected = TimeoutException.class)
+ public void noFoldIfAllOptionsAreDisabled() throws Exception {
+ bot.selectPreferencePage()
+ .uncheckPreference(FoldingBot.TABLE_FOLD_TARGET_PAREN)
+ .uncheckPreference(FoldingBot.TABLE_FOLD_TARGET_DOUBLEAPICES)
+ .ok()
+ .enableProjectionPreference()
+ .fillAndWaitForAnnotations(PROJECT_NAME, CORE_CLJ_NAME, MULTI_LINE_FORM);
+
+ bot.utils.clsActiveEditor().closeActiveEditor();
+ }
+
+ @Test
+ public void projectionShouldBeOnAfterEnablingPreference() throws Exception {
+ bot.utils.doubleClickOnFileInProject(PROJECT_NAME, CORE_CLJ_NAME);
+ bot.enableProjectionPreference().waitForProjectionEnabled()
+ .utils.closeActiveEditor();
+ }
+
+ @Test
+ public void projectionShouldBeOffAfterDisablingPreference() throws Exception {
+ bot.utils.doubleClickOnFileInProject(PROJECT_NAME, CORE_CLJ_NAME);
+ bot.disableProjectionPreference().waitForProjectionDisabled()
+ .utils.closeActiveEditor();
+ }
+
+ @Test
+ public void projectionShouldBeOnAfterDisableAndEnablePreference() throws Exception {
+ bot.utils.doubleClickOnFileInProject(PROJECT_NAME, CORE_CLJ_NAME);
+ bot.disableProjectionPreference()
+ .waitForProjectionDisabled()
+ .enableProjectionPreference()
+ .waitForProjectionEnabled()
+ .utils.closeActiveEditor();
+ }
+}
diff --git a/ccw.core.test/src/java/ccw/core/MenuLabels.java b/ccw.core.test/src/java/ccw/core/MenuLabels.java
index 33add194..3511d3d8 100644
--- a/ccw.core.test/src/java/ccw/core/MenuLabels.java
+++ b/ccw.core.test/src/java/ccw/core/MenuLabels.java
@@ -56,4 +56,8 @@ public class MenuLabels {
public static final String RESET_PROJECT_CONFIGURATION = "Reset project configuration";
public static final String REMOVE_LEIN_NATURE = "Remove Leiningen Support";
public static final String LAUNCH_HEADLESS_REPL = "Launch Headless REPL for the project";
+
+ // Window
+ public static final String WINDOW = "Window";
+ public static final String PREFERENCES = "Preferences";
}
diff --git a/ccw.core.test/src/java/ccw/core/PrefStrings.java b/ccw.core.test/src/java/ccw/core/PrefStrings.java
new file mode 100644
index 00000000..a3897682
--- /dev/null
+++ b/ccw.core.test/src/java/ccw/core/PrefStrings.java
@@ -0,0 +1,17 @@
+package ccw.core;
+
+public class PrefStrings {
+
+ public static final String TITLE = "Preferences";
+
+ // General
+ public static final String TREE_ENTRY_GENERAL = "General";
+ public static final String TREE_ENTRY_GENERAL_EDITORS = "Editors";
+ public static final String TREE_ENTRY_GENERAL_TEXTEDITORS = "Text Editors";
+
+ // Clojure
+ public static final String TREE_ENTRY_CLOJURE = "Clojure";
+ public static final String TREE_ENTRY_CLOJURE_EDITOR = "Editor";
+ public static final String TREE_ENTRY_CLOJURE_EDITOR_HOVERS = "Hovers";
+ public static final String TREE_ENTRY_CLOJURE_EDITOR_FOLDING = "Folding";
+}
diff --git a/ccw.core.test/src/java/ccw/core/SmokeTests.java b/ccw.core.test/src/java/ccw/core/SmokeTests.java
index 01d117c5..f680aa31 100644
--- a/ccw.core.test/src/java/ccw/core/SmokeTests.java
+++ b/ccw.core.test/src/java/ccw/core/SmokeTests.java
@@ -67,4 +67,26 @@ public void swtbotDoesNotRunOnTheUIThread() throws Exception {
public void canShowTestGeneratorEntryInClojureMenu() throws Exception {
SWTBotAssert.assertVisible(bot.menu("Clojure", MenuLabels.TEST, MenuLabels.TEST_GENERATOR));
}
+
+ @Test
+ public void canOpenPreferences() throws Exception {
+ bot.openWindowPreferences().bot().shell(PrefStrings.TITLE);
+ bot.ok();
+ }
+
+ @Test
+ public void shouldSelectClojurePreferencePage() throws Exception {
+ bot.selectPreferencePage(PrefStrings.TREE_ENTRY_CLOJURE)
+ .assertPreferencePage(PrefStrings.TREE_ENTRY_CLOJURE)
+ .ok();
+ }
+
+ @Test
+ public void shouldSelectDeepPreferencePages() throws Exception {
+ bot.selectPreferencePage(PrefStrings.TREE_ENTRY_GENERAL,
+ PrefStrings.TREE_ENTRY_GENERAL_EDITORS,
+ PrefStrings.TREE_ENTRY_GENERAL_TEXTEDITORS)
+ .assertPreferencePage(PrefStrings.TREE_ENTRY_GENERAL_TEXTEDITORS)
+ .ok();
+ }
}
diff --git a/ccw.core.test/src/java/ccw/core/bots/FoldingBot.java b/ccw.core.test/src/java/ccw/core/bots/FoldingBot.java
new file mode 100644
index 00000000..3ccb9ae7
--- /dev/null
+++ b/ccw.core.test/src/java/ccw/core/bots/FoldingBot.java
@@ -0,0 +1,111 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Laurent Petit.
+ * 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:
+ * Andrea RICHIARDI - initial implementation
+ *******************************************************************************/
+package ccw.core.bots;
+
+import org.eclipse.jface.preference.IPreferenceStore;
+
+import waits.ProjectionAnnotationModelNotEmpty;
+import waits.ProjectionAnnotationModelNull;
+import ccw.CCWPlugin;
+import ccw.core.BotUtils;
+import ccw.core.PrefStrings;
+import ccw.preferences.PreferenceConstants;
+
+/**
+ * Bot used for executing actions about the folding feature
+ */
+public class FoldingBot {
+
+ public static final long TIMEOUT_PROJECT_ANNOTATION_MODEL = 10000;
+
+ public static final String TABLE_FOLD_TARGET_PAREN = "Parens";
+ public static final String TABLE_FOLD_TARGET_DOUBLEAPICES = "Double-apices";
+
+ public final BotUtils utils;
+
+ public FoldingBot(BotUtils utils) {
+ this.utils = utils;
+ }
+
+ public FoldingBot ok() {
+ utils.ok();
+ return this;
+ }
+
+ public FoldingBot selectPreferencePage() {
+ utils.selectPreferencePage(PrefStrings.TREE_ENTRY_CLOJURE,
+ PrefStrings.TREE_ENTRY_CLOJURE_EDITOR,
+ PrefStrings.TREE_ENTRY_CLOJURE_EDITOR_FOLDING);
+ return this;
+ }
+
+ public FoldingBot uncheckPreference(String preferenceFoldTarget) {
+ utils.bot().table().getTableItem(preferenceFoldTarget).uncheck();
+ return this;
+ }
+
+ public FoldingBot checkPreference(String preferenceFoldTarget) {
+ utils.bot().table().getTableItem(preferenceFoldTarget).check();
+ return this;
+ }
+
+ /**
+ * Fills up the given file in the given project with the given text.
+ * @param projectName A project name
+ * @param fileName A file name (see {@link BotUtils#doubleClickOnFileInProject(String, String)})
+ * @param text The text to insert (will overwrite the existing)
+ * @return
+ * @throws Exception
+ */
+ public FoldingBot fillAndWaitForAnnotations(String projectName, String fileName, String text) throws Exception {
+ utils.replaceTextInFile(projectName, fileName, text).focusActiveEditor();
+ return waitForAnnotations(); // Waits for the reconciler to trigger and fill the annotations up
+ }
+
+ /**
+ * Enable the projection feature on the active editor through the
+ * {@link PreferenceConstants#EDITOR_FOLDING_PROJECTION_ENABLED} preference.
+ * @return
+ * @throws Exception
+ */
+ public FoldingBot enableProjectionPreference() {
+ IPreferenceStore store = CCWPlugin.getDefault().getPreferenceStore();
+ store.setValue(PreferenceConstants.EDITOR_FOLDING_PROJECTION_ENABLED, true);
+ return this;
+ }
+
+ /**
+ * Disable the projection feature on the active editor through the
+ * {@link PreferenceConstants#EDITOR_FOLDING_PROJECTION_ENABLED} preference.
+ * @return
+ * @throws Exception
+ */
+ public FoldingBot disableProjectionPreference() {
+ IPreferenceStore store = CCWPlugin.getDefault().getPreferenceStore();
+ store.setValue(PreferenceConstants.EDITOR_FOLDING_PROJECTION_ENABLED, false);
+ return this;
+ }
+
+ public FoldingBot waitForAnnotations() throws Exception {
+ utils.bot.waitUntil(new ProjectionAnnotationModelNotEmpty(utils.editor().clojure), TIMEOUT_PROJECT_ANNOTATION_MODEL);
+ return this;
+ }
+
+ public FoldingBot waitForProjectionEnabled() throws Exception {
+ utils.bot.waitWhile(new ProjectionAnnotationModelNull(utils.editor().clojure), TIMEOUT_PROJECT_ANNOTATION_MODEL);
+ return this;
+ }
+
+ public FoldingBot waitForProjectionDisabled() throws Exception {
+ utils.bot.waitUntil(new ProjectionAnnotationModelNull(utils.editor().clojure), TIMEOUT_PROJECT_ANNOTATION_MODEL);
+ return this;
+ }
+}
diff --git a/ccw.core.test/src/java/waits/ProjectionAnnotationModelNotEmpty.java b/ccw.core.test/src/java/waits/ProjectionAnnotationModelNotEmpty.java
new file mode 100644
index 00000000..e4535aec
--- /dev/null
+++ b/ccw.core.test/src/java/waits/ProjectionAnnotationModelNotEmpty.java
@@ -0,0 +1,44 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Laurent Petit.
+ * 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:
+ * Andrea RICHIARDI - initial implementation
+ *******************************************************************************/
+package waits;
+
+import org.eclipse.swtbot.swt.finder.finders.UIThreadRunnable;
+import org.eclipse.swtbot.swt.finder.results.BoolResult;
+import org.eclipse.swtbot.swt.finder.utils.internal.Assert;
+import org.eclipse.swtbot.swt.finder.waits.DefaultCondition;
+
+import ccw.editors.clojure.IClojureEditor;
+
+/**
+ * A condition that tests whether the ProjectionAnnotationModel
+ * on the ClojureEditor has at least one element.
+ */
+public class ProjectionAnnotationModelNotEmpty extends DefaultCondition {
+
+ private final IClojureEditor clojureEditor;
+
+ public ProjectionAnnotationModelNotEmpty(IClojureEditor ce) {
+ Assert.isNotNull(ce, "ClojureEditor was null"); //$NON-NLS-1$
+ this.clojureEditor = ce;
+ }
+
+ public String getFailureMessage() {
+ return "ClojureEditor's ProjectionAnnotationModel was never filled up."; //$NON-NLS-1$
+ }
+
+ public boolean test() throws Exception {
+ return UIThreadRunnable.syncExec(new BoolResult() {
+ public Boolean run() {
+ return clojureEditor.getProjectionAnnotationModel().getAnnotationIterator().hasNext();
+ }
+ });
+ }
+}
diff --git a/ccw.core.test/src/java/waits/ProjectionAnnotationModelNull.java b/ccw.core.test/src/java/waits/ProjectionAnnotationModelNull.java
new file mode 100644
index 00000000..75d411ae
--- /dev/null
+++ b/ccw.core.test/src/java/waits/ProjectionAnnotationModelNull.java
@@ -0,0 +1,44 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Laurent Petit.
+ * 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:
+ * Andrea RICHIARDI - initial implementation
+ *******************************************************************************/
+package waits;
+
+import org.eclipse.swtbot.swt.finder.finders.UIThreadRunnable;
+import org.eclipse.swtbot.swt.finder.results.BoolResult;
+import org.eclipse.swtbot.swt.finder.utils.internal.Assert;
+import org.eclipse.swtbot.swt.finder.waits.DefaultCondition;
+
+import ccw.editors.clojure.IClojureEditor;
+
+/**
+ * A condition that tests whether the ProjectionAnnotationModel
+ * on the ClojureEditor is null.
+ */
+public class ProjectionAnnotationModelNull extends DefaultCondition {
+
+ private final IClojureEditor clojureEditor;
+
+ public ProjectionAnnotationModelNull(IClojureEditor ce) {
+ Assert.isNotNull(ce, "ClojureEditor was null"); //$NON-NLS-1$
+ this.clojureEditor = ce;
+ }
+
+ public String getFailureMessage() {
+ return "ClojureEditor's ProjectionAnnotationModel was always null."; //$NON-NLS-1$
+ }
+
+ public boolean test() throws Exception {
+ return UIThreadRunnable.syncExec(new BoolResult() {
+ public Boolean run() {
+ return clojureEditor.getProjectionAnnotationModel() == null;
+ }
+ });
+ }
+}
diff --git a/ccw.core.test/src/java/waits/ResourceInSync.java b/ccw.core.test/src/java/waits/ResourceInSync.java
new file mode 100644
index 00000000..d3415d98
--- /dev/null
+++ b/ccw.core.test/src/java/waits/ResourceInSync.java
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Laurent Petit.
+ * 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:
+ * Andrea RICHIARDI - initial implementation
+ *******************************************************************************/
+package waits;
+
+import org.eclipse.core.resources.IResource;
+import org.eclipse.swtbot.swt.finder.finders.UIThreadRunnable;
+import org.eclipse.swtbot.swt.finder.results.BoolResult;
+import org.eclipse.swtbot.swt.finder.utils.internal.Assert;
+import org.eclipse.swtbot.swt.finder.waits.DefaultCondition;
+
+/**
+ * A condition that waits for the ProjectionAnnotationModel
+ * on the ClojureEditor has one element.
+ */
+public class ResourceInSync extends DefaultCondition {
+
+ private final IResource resource;
+
+ public ResourceInSync(IResource res) {
+ Assert.isNotNull(res, "IResource was null"); //$NON-NLS-1$
+ this.resource = res;
+ }
+
+ public String getFailureMessage() {
+ return "IResource never got in sync."; //$NON-NLS-1$
+ }
+
+ public boolean test() throws Exception {
+ return UIThreadRunnable.syncExec(new BoolResult() {
+ public Boolean run() {
+ return resource.isSynchronized(IResource.DEPTH_INFINITE);
+ }
+ });
+ }
+}
diff --git a/ccw.core/.options b/ccw.core/.options
index cfffa145..cd6603ea 100644
--- a/ccw.core/.options
+++ b/ccw.core/.options
@@ -74,8 +74,11 @@ ccw.core/eclipse=false
# Clojure Editor related traces
ccw.core/editor=false
-# Clojure Editor, partitioner (within scanners package) traces
-ccw.core/editor/scanners/partitioners=false
+# Clojure Editor, scanners package traces
+ccw.core/editor/scanners=false
+
+# Clojure Editor, text package traces
+ccw.core/editor/text=false
# Eclipse project management related traces
ccw.core/project=false
diff --git a/ccw.core/plugin.properties b/ccw.core/plugin.properties
index 52c45610..0a63a99d 100644
--- a/ccw.core/plugin.properties
+++ b/ccw.core/plugin.properties
@@ -103,6 +103,7 @@ preferencePage.syntaxColoring.name=Colors and Fonts
preferencePage.repl.name=REPL View
preferencePage.repl.history.name=REPL History
preferencePage.hover.name=Hovers
+preferencePage.folding.name=Folding
ExpandSelectionTo=Expand Selection To
ClojureSelectEnclosing_label=Enclosing Element
diff --git a/ccw.core/plugin.xml b/ccw.core/plugin.xml
index 9e63eebe..490a785e 100644
--- a/ccw.core/plugin.xml
+++ b/ccw.core/plugin.xml
@@ -1733,6 +1733,12 @@
class="ccw.preferences.HoverPreferencePage"
id="ccw.preferences.HoverPreferencePage">
+
+
diff --git a/ccw.core/src/clj/ccw/api/hyperlink.clj b/ccw.core/src/clj/ccw/api/hyperlink.clj
index c772538a..a72e50e6 100644
--- a/ccw.core/src/clj/ccw/api/hyperlink.clj
+++ b/ccw.core/src/clj/ccw/api/hyperlink.clj
@@ -127,7 +127,7 @@
[]
(let [get-source-viewer-configuration (set-method-accessible! org.eclipse.ui.texteditor.AbstractTextEditor "getSourceViewerConfiguration")
get-source-viewer (set-method-accessible! org.eclipse.ui.texteditor.AbstractTextEditor "getSourceViewer")]
- (let [editors (e/open-editors e/text-editor)]
+ (let [editors (e/open-editor-refs e/text-editor)]
(doseq [e editors
:let [e (e/text-editor e)
source-viewer-conf (invoke-method get-source-viewer-configuration e)
diff --git a/ccw.core/src/clj/ccw/eclipse.clj b/ccw.core/src/clj/ccw/eclipse.clj
index 28ec3014..0cac0315 100644
--- a/ccw.core/src/clj/ccw/eclipse.clj
+++ b/ccw.core/src/clj/ccw/eclipse.clj
@@ -53,6 +53,7 @@
[org.eclipse.jface.preference IPreferenceStore]
[org.eclipse.core.commands ExecutionEvent]
[org.eclipse.ui.actions WorkspaceModifyDelegatingOperation]
+ [org.eclipse.core.runtime.jobs Job]
[java.io File IOException]
[ccw CCWPlugin]
[ccw.util PlatformUtil]
@@ -139,7 +140,7 @@
([^IWorkbenchPage workbench-page] (page-editors workbench-page)))
(defn text-editor
- "Return theeditor associated to the reference if it's instanciated
+ "Return the editor associated to the reference if it's instantiated
and it's a text editor. Useful to only work with editors that have
been configured and you may want to reconfigure"
[e]
@@ -147,10 +148,10 @@
(when (instance? org.eclipse.ui.texteditor.AbstractTextEditor e)
e)))
-(defn open-editors
+(defn open-editor-refs
"Return all open editors of the Workbench.
pred is an optional predicate for filtering editors"
- ([] (open-editors identity))
+ ([] (open-editor-refs identity))
([pred]
(into []
(for [w (workbench-windows)
@@ -778,6 +779,20 @@
(catch Exception e
(CCWPlugin/createErrorStatus (format "Unexpected exception while executing Job %s" name), e))))))
+(defn job
+ "Create a Job with name, and delegate run to (f monitor). The method
+ returns a Job instance that, when executed, takes care of returning
+ an IStatus, if returned by f, or Status/OK_STATUS instead."
+ [name f]
+ (proxy [Job] [name]
+ (run [^IProgressMonitor monitor]
+ (try
+ (let [s (f monitor)]
+ (if (instance? IStatus s)
+ s
+ (Status/OK_STATUS)))
+ (catch Exception e
+ (CCWPlugin/createErrorStatus (format "Unexpected exception while executing Job %s" name), e))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Preferences management utilities
@@ -865,17 +880,19 @@
(defn ^{:author "Andrea Richiardi"}
add-preference-listener
- "Adds a listener in order to react to changes in the pref-key preference and executes
- the given closure if any. Note that this function generates a IPropertyChangeListener
- every time it is invoked. The version without preference store defaults toccw-combined-prefs."
- ([pref-key closure]
- (add-preference-listener (ccw-combined-prefs) pref-key closure))
- ([^IPreferenceStore pref-store pref-key closure]
+ "Adds a listener in order to react to changes in the pref-key
+ preference and executes (f event) when it does. Note that this
+ function generates a IPropertyChangeListener every time it is
+ invoked. The version without preference store defaults to
+ eclipse/ccw-combined-prefs."
+ ([pref-key f]
+ (add-preference-listener (ccw-combined-prefs) pref-key f))
+ ([^IPreferenceStore pref-store pref-key f]
(add-property-listener pref-store #(reify
IPropertyChangeListener
(propertyChange [this event]
(when (= (.getProperty event) pref-key)
- (closure)))))))
+ (f event)))))))
(def ^{:author "Andrea Richiardi" }
remove-preference-listener
diff --git a/ccw.core/src/clj/ccw/editors/clojure/ClojureTopLevelFormsDamagerImpl.clj b/ccw.core/src/clj/ccw/editors/clojure/ClojureTopLevelFormsDamagerImpl.clj
index dc2f1209..e59ffc69 100644
--- a/ccw.core/src/clj/ccw/editors/clojure/ClojureTopLevelFormsDamagerImpl.clj
+++ b/ccw.core/src/clj/ccw/editors/clojure/ClojureTopLevelFormsDamagerImpl.clj
@@ -17,7 +17,8 @@
(:use [paredit.utils :as utils])
(:import [org.eclipse.jface.text IRegion ITypedRegion DocumentEvent Region
IDocument]
- [ccw.editors.clojure ClojureTopLevelFormsDamager IClojureEditor])
+ ccw.editors.clojure.text.ClojureTopLevelFormsDamager
+ ccw.editors.clojure.IClojureEditor)
(:require [ccw.editors.clojure.editor-support :as editor]
[paredit.parser :as p]
[ccw.core.trace :as t]))
diff --git a/ccw.core/src/clj/ccw/editors/clojure/editor_support.clj b/ccw.core/src/clj/ccw/editors/clojure/editor_support.clj
index 20372a72..a1555b8a 100644
--- a/ccw.core/src/clj/ccw/editors/clojure/editor_support.clj
+++ b/ccw.core/src/clj/ccw/editors/clojure/editor_support.clj
@@ -70,6 +70,30 @@
)))
r))
+(defn init-text-buffer
+ "Initialize that input parse state with the input initial-text. Return
+ the new parse state."
+ [parse-state initial-text]
+ {:pre [(nil? parse-state)]
+ :post [(= (:build-id (deref %)) 0)]}
+ (let [parse-state (ref nil)
+ build-id 0]
+ (dosync
+ ;; AR - The buffer needs to be recalculated if the transaction is retried
+ (let [buffer (p/edit-buffer nil 0 -1 initial-text)
+ parse-tree (p/buffer-parse-tree buffer build-id)]
+ (ref-set parse-state {:text initial-text
+ :incremental-text-buffer buffer
+ :previous-parse-tree nil
+ :parse-tree parse-tree
+ :build-id build-id})))
+ (t/trace :editor (str "Parse state initialized!"
+ \newline
+ "text:" :text
+ \newline
+ "parse tree elements:" (count (:parse-tree @parse-state))))
+ parse-state))
+
(defn startWatchParseRef [r editor]
(add-watch r :track-state (fn [_ _ _ new-state]
(.setStructuralEditionPossible editor
@@ -142,3 +166,9 @@
Remember that passing nil as message resets the status line."
[^IClojureEditor part message]
(swt/doasync (.setStatusLineErrorMessage part message)))
+
+(defn open-clojure-editors
+ "Return all open clojure editors of the Workbench, pred is an optional
+ predicate for filtering them."
+ ([] (open-clojure-editors identity))
+ ([pred] (map (comp #(e/adapter % IClojureEditor) #(.getEditor % true)) (e/open-editor-refs identity))))
diff --git a/ccw.core/src/clj/ccw/editors/clojure/folding_support.clj b/ccw.core/src/clj/ccw/editors/clojure/folding_support.clj
new file mode 100644
index 00000000..46f3578e
--- /dev/null
+++ b/ccw.core/src/clj/ccw/editors/clojure/folding_support.clj
@@ -0,0 +1,364 @@
+;*******************************************************************************
+;* Copyright (c) 2015 Laurent PETIT.
+;* 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:
+;* Andrea Richiardi - initial implementation (code reviewed by Laurent Petit)
+;*******************************************************************************/
+(ns ^{:author "Andrea Richiardi"}
+ ccw.editors.clojure.folding-support
+ "Folding support functions"
+ (:refer-clojure :exclude [tree-seq read-string])
+ (:require [ccw.core.trace :refer [trace]]
+ [clojure.edn :as edn :refer [read-string]]
+ [clojure.java.data :refer :all]
+ [clojure.zip :as z]
+ [paredit.loc-utils :as lu]
+ [ccw.editors.clojure.editor-support :as es :refer [getParseTree
+ open-clojure-editors]]
+ [ccw.eclipse :as e :refer [boolean-ccw-pref
+ string-ccw-pref
+ preference!
+ job]]
+ [ccw.swt :as swt :refer [doasync]])
+ (:import org.eclipse.ui.texteditor.AbstractTextEditor
+ org.eclipse.e4.core.contexts.IEclipseContext
+ org.eclipse.core.databinding.observable.list.WritableList
+ org.eclipse.jface.text.IRegion
+ org.eclipse.jface.text.Position
+ org.eclipse.jface.text.source.Annotation
+ org.eclipse.jface.text.source.projection.ProjectionAnnotationModel
+ org.eclipse.jface.text.source.projection.ProjectionAnnotation
+ ccw.core.StaticStrings
+ ccw.editors.clojure.ClojureEditorMessages
+ ccw.editors.clojure.IClojureEditor
+ ccw.preferences.PreferenceConstants
+ ccw.editors.clojure.folding.FoldingModel
+ ccw.editors.clojure.folding.FoldingDescriptor))
+
+(defn- any-enabled?
+ "Return true if at least one descriptor is enabled, false otherwise."
+ [descriptors]
+ (not (not-any? :enabled descriptors)))
+
+(defn- enabled-tags
+ "Collect and return a set of the loc tags that are associated with
+ enabled descriptors."
+ [descriptors]
+ (into #{} (reduce concat (map :loc-tags (filter :enabled descriptors)))))
+
+(defn- locs-with-tags
+ "Given an already parsed tree (root loc in paredit) and a set of tags,
+ returns the seq of locs which contains them or nil if no match was
+ found."
+ [rloc tag-set]
+ (seq (filter #(get tag-set (lu/loc-tag %1)) (lu/next-leaves rloc))))
+
+(defn- loc->position
+ [loc]
+ (let [start-offset (lu/start-offset loc)
+ end-offset (lu/end-offset loc)]
+ (Position. start-offset (- end-offset start-offset))))
+
+(defn- pos->vec
+ "Given a org.eclipse.jface.text.Position, build a vector [offset
+ length]."
+ [^Position pos]
+ [(.offset pos) (.length pos)])
+
+(defn- seq->pos
+ "Given a seq of two elements which represent offset and length, build
+ a org.eclipse.jface.text.Position."
+ [coll]
+ (Position. (first coll) (second coll)))
+
+;; (defn enclose?
+ ;; "Return true if pos1 encloses pos2. This implementation returns false
+ ;; if the Positions have same start/end offset (e.g.: [2 40] does not
+ ;; include [2 30] and [4 10] does not include [5 9])."
+ ;; [^Position pos1 ^Position pos2]
+ ;; (let [of1 (.offset pos1)
+ ;; of2 (.offset pos2)
+ ;; ln1 (.length pos1)
+ ;; ln2 (.length pos2)]
+ ;; (cond
+ ;; (<= of2 of1) false
+ ;; (>= (+ of2 ln2) (+ of1 ln1)) false
+ ;; :else true)))
+
+;; (defn- overlap?
+ ;; "Return true if at least one position overlaps with another inside the
+ ;; positions parameter. False otherwise.
+ ;; Note1: The cases where the position vector is empty or contains only
+ ;; one element are considered not overlapping.
+ ;; Note2: if pos1 and pos2 have just boundaries in common they do not
+ ;; overlap."
+ ;; ([positions] (overlap? positions false))
+ ;; ([positions overlapping?]
+ ;; (cond
+ ;; overlapping? overlapping?
+ ;; (<= (count positions) 1) overlapping?
+ ;; :else (let [pos1 (first positions)
+ ;; pos-rest (rest positions)
+ ;; pos1-against-rest (for [pos2 pos-rest]
+ ;; (.overlapsWith pos1 (.offset pos2) (.length pos2)))]
+ ;; (recur pos-rest (not (every? false? pos1-against-rest)))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;
+;;; Parsing helpers ;;;
+;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn- inc-token-occ
+ "Increment the token :occurrences in the map."
+ [token-map token]
+ (update-in token-map [token :occurrences] inc))
+
+(defn- dec-token-occ
+ "Decrement the token :occurrences in the map."
+ [token-map token]
+ (update-in token-map [token :occurrences] dec))
+
+(defn- update-token-map
+ "Initialize the token map assigning {:loc token-loc and :occurrences}
+ to the input token."
+ [token-map token token-loc]
+ (let [old-token (get token-map token)]
+ (if old-token
+ (inc-token-occ token-map token)
+ (assoc token-map token {:loc token-loc :occurrences 1}))))
+
+(defn- token-data-info
+ "Convert token data {:loc :occurrences ...} into a human readable
+ map which will contain useful info only. Useful for debugging."
+ [token-data]
+ (let [l (:loc token-data)
+ occ (:occurrences token-data)]
+ {:text (lu/loc-text l)
+ :occs occ
+ :start (lu/start-offset l)
+ :end (lu/end-offset l)}))
+
+(defn- next-is-newline?
+ "Given a loc, return true if the next loc is a newline (according to
+ paredit's loc-utils). False otherwise."
+ [loc]
+ (lu/newline? (z/next loc)))
+
+(def ^:private son-of-a-multi-line-loc?
+ "Return true if the input loc is children of a multi-line loc, that is,
+ its zip/up is on multiple lines, according to paredit
+ single-line-loc?. False otherwise."
+ (complement (comp lu/single-line-loc? z/up)))
+
+(defn- folding-range
+ "Compute the folding range given the loc of the opening token and
+ the loc of the closing token. Returns org.eclipse.jface.text.Position
+ or nil if something goes wrong."
+ [open-token-loc close-token-loc]
+ (let [open-token-end (lu/end-offset open-token-loc)
+ close-token-end (lu/end-offset close-token-loc)]
+ (trace :editor/text (str "computing folding for '" (lu/loc-text open-token-loc) "' -> offset end at " open-token-end
+ " and '" (lu/loc-text close-token-loc) "' -> offset start at " close-token-end))
+ ; TODO - AR handle custom positions instances (see jdt's JavaElementPosition)
+ (if (> close-token-end open-token-end)
+ (Position. open-token-end (- close-token-end open-token-end))
+ nil)))
+
+(declare match-paren
+ match-double-apex
+ ;; match-bracket
+ )
+
+(defn- parse-locs
+ "Give a loc seq, extract the locs that are going to be used for folding.
+ Returns another seq or nil. This function has been implemented for
+ using through the tail-call optimized trampoline and carries two
+ accumulating parameters.
+ 1) open-token-loc-map will accumulate the open token
+ data (:loc :occurrences ...)
+ 2) positions will accumulate the positions used for folding"
+ [loc-seq open-token-map positions]
+ (if (seq loc-seq)
+ (let [loc (first loc-seq)
+ loc-token (lu/loc-text loc)]
+ (trace :editor/text (str "current -> token " loc-token " start " (lu/start-offset loc) " end " (lu/end-offset loc) "\n"
+ "token-map -> " (pr-str (map #(token-data-info (second %1)) open-token-map))))
+ #(case loc-token
+ "(" (match-paren (rest loc-seq) (update-token-map open-token-map "(" loc) positions)
+ "\"" (match-double-apex (rest loc-seq) (update-token-map open-token-map "\"" loc) positions)
+ ")" (match-paren loc-seq open-token-map positions) ))
+ positions))
+
+(defn- match-paren
+ [loc-seq open-token-map positions]
+ (if (seq loc-seq)
+ (let [loc (first loc-seq)
+ loc-token (lu/loc-text loc)]
+ (trace :editor/text (str "current -> token " loc-token " start " (lu/start-offset loc) " end " (lu/end-offset loc) "\n"
+ "token-map -> " (pr-str (map #(token-data-info (second %1)) open-token-map))))
+ #(case loc-token
+ ;; Match
+ ")" (let [open-token-data (get open-token-map "(")]
+ (if (= (:occurrences open-token-data) 1)
+ (parse-locs (rest loc-seq) (dissoc open-token-map "(")
+ (conj positions (folding-range (:loc open-token-data) loc)))
+ (match-paren (rest loc-seq) (dec-token-occ open-token-map "(") positions)))
+ ;; Other cases
+ "(" (match-paren (rest loc-seq) (inc-token-occ open-token-map "(") positions)
+ "\"" (match-double-apex (rest loc-seq) (update-token-map open-token-map "\"" loc) positions)))
+ positions))
+
+(defn- match-double-apex
+ [loc-seq open-token-map positions]
+ (if (seq loc-seq)
+ (let [loc (first loc-seq)
+ loc-token (lu/loc-text loc)]
+ (trace :editor/text (str "current -> token " loc-token " start " (lu/start-offset loc) " end " (lu/end-offset loc) "\n"
+ "token-map -> " (pr-str (map #(token-data-info (second %1)) open-token-map))))
+ #(case loc-token
+ ;; Match
+ "\"" (if-let [open-token-data (get open-token-map "\"")]
+ (if (= (:occurrences open-token-data) 1)
+ (parse-locs (rest loc-seq) (dissoc open-token-map "\"")
+ (conj positions (folding-range (:loc open-token-data) loc)))
+ (match-double-apex (rest loc-seq) (dec-token-occ open-token-map "\"") positions)))
+ ;; Other cases
+ "(" (match-paren (rest loc-seq) (inc-token-occ open-token-map "(") positions)))
+ positions))
+
+(defn- folding-positions
+ "Compute a set of positions (offset, length, wrapped in a
+ org.eclipse.jface.text.Position) of the folding point according to the
+ folding descriptors."
+ [parse-state descriptors]
+ (when (any-enabled? descriptors)
+ (let [parse-tree (es/getParseTree parse-state)
+ root-loc (lu/parsed-root-loc parse-tree)
+ loc-tags (enabled-tags descriptors)
+ loc-seq (locs-with-tags root-loc loc-tags)
+ multi-line-locs (filter son-of-a-multi-line-loc? loc-seq)] ; TODO - get set from preference
+ (into #{} (keep identity (trampoline parse-locs multi-line-locs {} ()))))))
+
+;;;;;;;;;;;;;;;;;;;
+;;; Java Interop ;;
+;;;;;;;;;;;;;;;;;;;
+
+(defmethod to-java [FoldingDescriptor clojure.lang.APersistentMap] [clazz props]
+ (doto (FoldingDescriptor.
+ (name (:id props))
+ (:label props)
+ (:enabled props)
+ (:description props)
+ (:loc-tags props))))
+
+(defmethod from-java FoldingDescriptor [^FoldingDescriptor instance]
+ {:id (keyword (.getId instance))
+ :label (.getLabel instance)
+ :enabled (.isEnabled instance)
+ :description (.getDescription instance)
+ :loc-tags (.getTags instance)})
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Preference helpers ;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn- read-descriptors-preference!
+ "Load the descriptors from the preferences."
+ []
+ (edn/read-string {:eof ""} (e/string-ccw-pref PreferenceConstants/EDITOR_FOLDING_DESCRIPTORS)))
+
+(defn- read-projection-enabled-preference!
+ "Load whether the projection is enabled from the preferences, returns
+ a Boolean."
+ []
+ (e/boolean-ccw-pref PreferenceConstants/EDITOR_FOLDING_PROJECTION_ENABLED))
+
+(defn- persist-descriptors!
+ "Persist the input descriptors in the preferences."
+ [descriptors]
+ (e/preference! PreferenceConstants/EDITOR_FOLDING_DESCRIPTORS (pr-str descriptors)))
+
+(defn- toggle-folding-on-editors!
+ "Toggle the folding on all the open editors. The parameter enabled must be
+ a Boolean."
+ [enabled]
+ (when-let [editors (seq (es/open-clojure-editors))]
+ (trace :editor/text (str "Toggling folding on all the open IClojureEditor to " enabled))
+ (doseq [^IClojureEditor e editors]
+ (.enableProjection e enabled))))
+
+(defn- disable-then-enable-folding-on-editors!
+ "Enable and then disable the folding on all the open editors. This
+ solves the issue #799 - Problem with dark editor theme."
+ []
+ (when (read-projection-enabled-preference!)
+ (trace :editor/text (str "Performing workaround for issue #799 on all editors"))
+ (let [off-job (doto (e/job "Switch off folding - Workaround #799"
+ (fn [_] (swt/dosync (toggle-folding-on-editors! false))))
+ (.setSystem true))
+ on-job (doto (e/job "Switch on folding - Workaround #799"
+ (fn [_] (swt/dosync (toggle-folding-on-editors! true))))
+ (.setSystem true))]
+ (.schedule off-job 250)
+ (.schedule on-job 1500))))
+
+;;;;;;;;;;;;;;
+;;; Public ;;;
+;;;;;;;;;;;;;;
+
+(defn compute-folding!
+ "Update the ProjectionAnnotationModel's positions (offset, length,
+ wrapped in a org.eclipse.jface.text.Position) used for folding in the
+ input IClojureEditor. The UI part is executed using swt/doasync."
+ [^IClojureEditor editor]
+ (let [descriptors (read-descriptors-preference!)]
+ (if (and (read-projection-enabled-preference!) (any-enabled? descriptors))
+ (let [positions (folding-positions (.getParseState editor) descriptors)
+ additions (zipmap (repeatedly #(ProjectionAnnotation.)) positions)]
+ (trace :editor/text (str "computed additions: " additions))
+ ;; TODO - AR incrementally handle additions, removals
+ (let [result-promise (swt/doasync
+ (when-let [^ProjectionAnnotationModel model (some-> editor (.getProjectionAnnotationModel))]
+ (trace :editor/text "ProjectionAnnotationModel not null, updating annotations...")
+ (.removeAllAnnotations model)
+ (.modifyAnnotations model nil additions nil)
+ (.markDamagedAndRedraw editor)))]
+ (when (instance? Throwable @result-promise)
+ (trace :editor/text "Exception caught while updating the editor" @result-promise))))
+ (trace :editor/text (str "Either projection or descriptors were disabled, skipping folding computation")))))
+
+(defn init-injections
+ "Set a new instance of FoldingModel in the input context."
+ [^IEclipseContext context]
+ (trace :editor/text (str "Initializing folding injections..."))
+ (.set context StaticStrings/CCW_CONTEXT_VALUE_FOLDINGMODEL
+ (reify
+ FoldingModel
+ (getObservableDescriptors [this]
+ (WritableList.
+ (map #(to-java FoldingDescriptor %1) (read-descriptors-preference!))
+ FoldingDescriptor))
+
+ (persistDescriptors [this list]
+ (let [descriptors (map #(from-java %1) list)]
+ (persist-descriptors! descriptors))))))
+
+(defn any-descriptor-enabled?
+ "Return true if folding is enabled in the preferences, false
+ otherwise."
+ []
+ (any-enabled? (read-descriptors-preference!)))
+
+(defn add-preference-listeners
+ []
+ (let [pref-projection PreferenceConstants/EDITOR_FOLDING_PROJECTION_ENABLED
+ pref-background AbstractTextEditor/PREFERENCE_COLOR_BACKGROUND]
+ (trace :editor/text (str "Registering " pref-projection " and " pref-background " listeners..."))
+ (ccw.eclipse/add-preference-listener pref-projection
+ (fn [event] (toggle-folding-on-editors! (.getNewValue event))))
+ ;; AR - fix for issue #799
+ (ccw.eclipse/add-preference-listener pref-background
+ (fn [_] (disable-then-enable-folding-on-editors!)))))
diff --git a/ccw.core/src/clj/ccw/editors/clojure/hover_support.clj b/ccw.core/src/clj/ccw/editors/clojure/hover_support.clj
index abe410e8..567ed4da 100644
--- a/ccw.core/src/clj/ccw/editors/clojure/hover_support.clj
+++ b/ccw.core/src/clj/ccw/editors/clojure/hover_support.clj
@@ -469,11 +469,11 @@
(let [pref-key PreferenceConstants/EDITOR_TEXT_HOVER_DESCRIPTORS]
(trace :support/hover (str "Registering " pref-key " listener..."))
(ccw.eclipse/add-preference-listener pref-key
- #(do
- (trace :support/hover (str "Preference " pref-key " has changed. Resetting hovers..."))
- (swap! (force state-atom) set-state-dirty)
- ;; I need some protection because here i might not have active editor
- (some-> (CCWPlugin/getClojureEditor) reset-default-hover-on-editor!)))))
+ (fn [_]
+ (trace :support/hover (str "Preference " pref-key " has changed. Resetting hovers..."))
+ (swap! (force state-atom) set-state-dirty)
+ ;; I need some protection because here i might not have active editor
+ (some-> (CCWPlugin/getClojureEditor) reset-default-hover-on-editor!)))))
;;;;;;;;;;;;;;;;;;;
;;; Java Interop ;;
diff --git a/ccw.core/src/java/ccw/CCWPlugin.java b/ccw.core/src/java/ccw/CCWPlugin.java
index 03f6bfc2..80b9eff8 100644
--- a/ccw.core/src/java/ccw/CCWPlugin.java
+++ b/ccw.core/src/java/ccw/CCWPlugin.java
@@ -33,7 +33,6 @@
import org.eclipse.e4.core.contexts.IEclipseContext;
import org.eclipse.e4.core.services.events.IEventBroker;
import org.eclipse.e4.ui.workbench.UIEvents;
-import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.PreferenceConverter;
import org.eclipse.jface.resource.ColorRegistry;
@@ -375,6 +374,10 @@ public void bundleChanged(BundleEvent evt) {
invoker._("add-registry-listener");
invoker._("add-preference-listener");
+ // Adding hover extension listener
+ invoker = ClojureInvoker.newInvoker(this, "ccw.editors.clojure.folding-support");
+ invoker._("add-preference-listeners");
+
log("CCWPlugin.start(): EXIT");
}
@@ -550,17 +553,14 @@ public static REPLView[] getREPLViews() {
public void run() {
IWorkbenchWindow window = PlatformUI.getWorkbench().getActiveWorkbenchWindow();
if (window != null) {
- IWorkbenchPage[] pages = window.getPages();
- for (int i = 0; i < pages.length; i++) {
- IWorkbenchPage page = pages[i];
- if (page != null) {
- for (IViewReference r : page.getViewReferences()) {
- IViewPart v = r.getView(false);
- if (REPLView.class.isInstance(v)) {
- ret.add((REPLView) v);
- }
- }
- }
+ IWorkbenchPage page = window.getActivePage();
+ if (page != null) {
+ for (IViewReference r : page.getViewReferences()) {
+ IViewPart v = r.getView(false);
+ if (REPLView.class.isInstance(v)) {
+ ret.add((REPLView) v);
+ }
+ }
}
}
}
@@ -741,6 +741,7 @@ private void initInjections(BundleContext bundleContext) {
IEclipseContext c = EclipseContextFactory.getServiceContext(bundleContext);
ClojureInvoker.newInvoker(this, "ccw.editors.clojure.hover-support")._("init-injections", c);
+ ClojureInvoker.newInvoker(this, "ccw.editors.clojure.folding-support")._("init-injections", c);
}
private void cleanInjections() {
diff --git a/ccw.core/src/java/ccw/TraceOptions.java b/ccw.core/src/java/ccw/TraceOptions.java
index 209029a6..a276be11 100644
--- a/ccw.core/src/java/ccw/TraceOptions.java
+++ b/ccw.core/src/java/ccw/TraceOptions.java
@@ -84,9 +84,12 @@ public class TraceOptions {
/** Clojure Editor related traces */
public static final String EDITOR = "/editor";
- /** Clojure Editor, partitioner (within scanners package) traces */
- public static final String PARTITIONERS = "/editor/scanners/partitioners";
+ /** Clojure Editor, scanners traces */
+ public static final String EDITOR_SCANNERS = "/editor/scanners";
+ /** Clojure Editor, texts package traces */
+ public static final String EDITOR_TEXT = "/editor/text";
+
/** Eclipse project management related traces */
public static final String PROJECT = "/project";
diff --git a/ccw.core/src/java/ccw/core/StaticStrings.java b/ccw.core/src/java/ccw/core/StaticStrings.java
index 812564df..e4d1c260 100644
--- a/ccw.core/src/java/ccw/core/StaticStrings.java
+++ b/ccw.core/src/java/ccw/core/StaticStrings.java
@@ -19,7 +19,8 @@ public class StaticStrings {
// Context
public static final String CCW_CONTEXT_VALUE_HOVERMODEL = "ccw.context.value.hover-model";
-
+ public static final String CCW_CONTEXT_VALUE_FOLDINGMODEL = "ccw.context.value.folding-model";
+
// Files
public final static String CCW_HOVER_CSS = "platform:/plugin/" + CCWPlugin.PLUGIN_ID + "/assets/hover-control-style.css";
diff --git a/ccw.core/src/java/ccw/editors/clojure/BasicClojureEditorActionContributor.java b/ccw.core/src/java/ccw/editors/clojure/BasicClojureEditorActionContributor.java
index 46b5d534..fb498dfc 100644
--- a/ccw.core/src/java/ccw/editors/clojure/BasicClojureEditorActionContributor.java
+++ b/ccw.core/src/java/ccw/editors/clojure/BasicClojureEditorActionContributor.java
@@ -34,6 +34,8 @@
import org.eclipse.ui.texteditor.RetargetTextEditorAction;
import org.eclipse.ui.texteditor.StatusLineContributionItem;
+import ccw.editors.clojure.folding.FoldingActionGroup;
+
/**
* Common base class for action contributors for Clojure editors.
*/
@@ -176,6 +178,13 @@ public void setActiveEditor(IEditorPart part) {
}
showInformationAction.setAction(getAction(textEditor, ITextEditorActionConstants.SHOW_INFORMATION));
+
+ if (part instanceof ClojureEditor) {
+ FoldingActionGroup foldingActions= ((ClojureEditor)part).getFoldingActionGroup();
+ if (foldingActions != null) {
+ foldingActions.updateActionBars();
+ }
+ }
}
@Override
diff --git a/ccw.core/src/java/ccw/editors/clojure/ClojureDocumentProvider.java b/ccw.core/src/java/ccw/editors/clojure/ClojureDocumentProvider.java
index 2365cbcf..5909235f 100644
--- a/ccw.core/src/java/ccw/editors/clojure/ClojureDocumentProvider.java
+++ b/ccw.core/src/java/ccw/editors/clojure/ClojureDocumentProvider.java
@@ -23,7 +23,7 @@
import ccw.StorageMarkerAnnotationModel;
import ccw.editors.clojure.scanners.ClojurePartitionScanner;
-import ccw.editors.clojure.scanners.ClojurePartitioner;
+import ccw.editors.clojure.scanners.TracingPartitioner;
public class ClojureDocumentProvider extends TextFileDocumentProvider {
@@ -33,7 +33,7 @@ public class ClojureDocumentProvider extends TextFileDocumentProvider {
* by this provider in that case).
*/
public static IDocument configure (IDocument document) {
- IDocumentPartitioner partitioner = new ClojurePartitioner(new ClojurePartitionScanner(),
+ IDocumentPartitioner partitioner = new TracingPartitioner(new ClojurePartitionScanner(),
ClojurePartitionScanner.CLOJURE_CONTENT_TYPES);
Map m = new HashMap();
diff --git a/ccw.core/src/java/ccw/editors/clojure/ClojureEditor.java b/ccw.core/src/java/ccw/editors/clojure/ClojureEditor.java
index dde90412..a4e45c32 100644
--- a/ccw.core/src/java/ccw/editors/clojure/ClojureEditor.java
+++ b/ccw.core/src/java/ccw/editors/clojure/ClojureEditor.java
@@ -19,6 +19,9 @@
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jface.action.Action;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
@@ -29,6 +32,7 @@
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.IVerticalRuler;
import org.eclipse.jface.text.source.SourceViewerConfiguration;
+import org.eclipse.jface.text.source.projection.ProjectionAnnotationModel;
import org.eclipse.jface.text.source.projection.ProjectionSupport;
import org.eclipse.jface.text.source.projection.ProjectionViewer;
import org.eclipse.jface.util.IPropertyChangeListener;
@@ -40,12 +44,15 @@
import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IPropertyListener;
import org.eclipse.ui.editors.text.TextEditor;
+import org.eclipse.ui.texteditor.ITextEditorActionConstants;
import org.eclipse.ui.texteditor.SourceViewerDecorationSupport;
import org.eclipse.ui.texteditor.StatusLineContributionItem;
import org.eclipse.ui.views.contentoutline.IContentOutlinePage;
import ccw.CCWPlugin;
import ccw.editors.clojure.ClojureSourceViewer.IStatusLineHandler;
+import ccw.editors.clojure.folding.FoldingActionGroup;
+import ccw.editors.clojure.folding.FoldingMessages;
import ccw.editors.clojure.scanners.ClojurePartitionScanner;
import ccw.editors.outline.ClojureOutlinePage;
import ccw.launching.ClojureLaunchShortcut;
@@ -206,7 +213,7 @@ protected ISourceViewer createSourceViewer(Composite parent, IVerticalRuler rule
fProjectionSupport.addSummarizableAnnotationType("org.eclipse.ui.workbench.texteditor.warning"); //$NON-NLS-1$
fProjectionSupport.install();
-
+
// TODO Add the hovers on the Projection?
// fProjectionSupport.setHoverControlCreator(new IInformationControlCreator() {
// public IInformationControl createInformationControl(Shell shell) {
@@ -219,8 +226,6 @@ protected ISourceViewer createSourceViewer(Composite parent, IVerticalRuler rule
// }
// });
}
-
- viewer.doOperation(ClojureSourceViewer.TOGGLE);
// ensure decoration support has been created and configured.
SourceViewerDecorationSupport sourceViewerDecorationSupport = getSourceViewerDecorationSupport(viewer);
@@ -296,6 +301,19 @@ public ISelectionProvider getSelectionProvider() {
return getSourceViewer().getSelectionProvider();
}
+ /**
+ * The action group for folding.
+ */
+ private FoldingActionGroup fFoldingGroup;
+
+ /**
+ * Returns the folding action group, or null if there is none.
+ * @return the ActionGroup or null;
+ */
+ protected @Nullable FoldingActionGroup getFoldingActionGroup() {
+ return fFoldingGroup;
+ }
+
@Override
protected void createActions() {
super.createActions();
@@ -363,9 +381,29 @@ public void run() {
action.setActionDefinitionId(ClojureSourceViewer.STATUS_CATEGORY_STRUCTURAL_EDITION);
setAction(ClojureSourceViewer.STATUS_CATEGORY_STRUCTURAL_EDITION, action);
-}
-
+ fFoldingGroup = new FoldingActionGroup(this, getSourceViewer(), CCWPlugin.getDefault().getPreferenceStore());
+ }
+ /*
+ * @see org.eclipse.ui.texteditor.AbstractTextEditor#rulerContextMenuAboutToShow(org.eclipse.jface.action.IMenuManager)
+ */
+ @Override
+ protected void rulerContextMenuAboutToShow(IMenuManager menu) {
+ super.rulerContextMenuAboutToShow(menu);
+ IMenuManager foldingMenu= new MenuManager(FoldingMessages.getString("Projection_FoldingMenu_name"), "projection"); //$NON-NLS-1$
+ menu.appendToGroup(ITextEditorActionConstants.GROUP_RULERS, foldingMenu);
+
+ IAction action= getAction("FoldingToggle"); //$NON-NLS-1$
+ foldingMenu.add(action);
+ action= getAction("FoldingExpandAll"); //$NON-NLS-1$
+ foldingMenu.add(action);
+ action= getAction("FoldingCollapseAll"); //$NON-NLS-1$
+ foldingMenu.add(action);
+// TODO action= getAction("FoldingRestore"); //$NON-NLS-1$
+// foldingMenu.add(action);
+// TODO action= getAction("FoldingCollapseComments"); //$NON-NLS-1$
+// foldingMenu.add(action);
+ }
/**
* Move to beginning of current or preceding defun (beginning-of-defun).
@@ -423,10 +461,6 @@ public IDocument getDocument() {
return sourceViewer.getDocument();
}
- /**
- * Asserts document != null.
- * @return
- */
public int getSourceCaretOffset() {
IRegion selection= getSignedSelection();
return selection.getOffset() + selection.getLength();
@@ -795,4 +829,34 @@ public void initializeViewerColors() {
sourceViewer().initializeViewerColors();
}
+ /* (non-Javadoc)
+ * @see ccw.editors.clojure.IClojureEditor#enableProjection(boolean)
+ */
+ @Override
+ public void enableProjection(boolean enabled) {
+ sourceViewer().enableProjection(enabled);
+ }
+
+ /**
+ * Workaround that toggles again the Projection on the viewer if it is
+ * not already on but the preference commands it so.
+ */
+ private void ensureProjectionState() {
+ // AR - forcing enabling when if preference is on
+ boolean enabled = getPreferenceStore().getBoolean(PreferenceConstants.EDITOR_FOLDING_PROJECTION_ENABLED);
+ if (enabled) {
+ ClojureSourceViewer viewer = sourceViewer();
+ if (!viewer.isProjectionMode() || viewer.getProjectionAnnotationModel() == null) {
+ viewer.enableProjection();
+ }
+ }
+ }
+
+ /* (non-Javadoc)
+ * @see ccw.editors.clojure.IClojureEditor#getProjectionAnnotationModel()
+ */
+ public @Nullable ProjectionAnnotationModel getProjectionAnnotationModel() {
+ ensureProjectionState();
+ return sourceViewer().getProjectionAnnotationModel();
+ }
}
diff --git a/ccw.core/src/java/ccw/editors/clojure/ClojureSourceViewer.java b/ccw.core/src/java/ccw/editors/clojure/ClojureSourceViewer.java
index 7fd3dab4..8b7b8395 100644
--- a/ccw.core/src/java/ccw/editors/clojure/ClojureSourceViewer.java
+++ b/ccw.core/src/java/ccw/editors/clojure/ClojureSourceViewer.java
@@ -677,4 +677,13 @@ public void removePropertyChangeListener(IPropertyChangeListener listener) {
String form = (String) editorSupport._("top-level-code-form", getParseState(), caretOffset);
return form;
}
+
+ @Override
+ public void enableProjection(boolean enabled) {
+ if (isProjectionMode() != enabled) {
+ if (canDoOperation(ProjectionViewer.TOGGLE)) {
+ doOperation(ProjectionViewer.TOGGLE);
+ }
+ }
+ }
}
diff --git a/ccw.core/src/java/ccw/editors/clojure/ClojureSourceViewerConfiguration.java b/ccw.core/src/java/ccw/editors/clojure/ClojureSourceViewerConfiguration.java
index 75a69c36..e467b8b6 100644
--- a/ccw.core/src/java/ccw/editors/clojure/ClojureSourceViewerConfiguration.java
+++ b/ccw.core/src/java/ccw/editors/clojure/ClojureSourceViewerConfiguration.java
@@ -37,11 +37,16 @@
import org.eclipse.jface.text.information.IInformationProvider;
import org.eclipse.jface.text.information.InformationPresenter;
import org.eclipse.jface.text.reconciler.IReconciler;
+import org.eclipse.jface.text.reconciler.IReconcilingStrategy;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.swt.widgets.Shell;
import ccw.CCWPlugin;
import ccw.editors.clojure.scanners.ClojurePartitionScanner;
+import ccw.editors.clojure.text.ClojureCompositeReconcilingStrategy;
+import ccw.editors.clojure.text.ClojureReconciler;
+import ccw.editors.clojure.text.CompositeReconcilingStrategy;
+import ccw.editors.clojure.text.FoldingReconcileStrategy;
import ccw.preferences.PreferenceConstants;
import ccw.util.ClojureInvoker;
@@ -53,16 +58,20 @@ public class ClojureSourceViewerConfiguration extends SimpleSourceViewerConfigur
private final ClojureInvoker proposalProcessor = ClojureInvoker.newInvoker(
CCWPlugin.getDefault(),
"ccw.editors.clojure.clojure-proposal-processor");
-
+
ClojureInvoker hoverSupportInvoker = ClojureInvoker.newInvoker(CCWPlugin.getDefault(),
"ccw.editors.clojure.hover-support");
-
+
ClojureInvoker doubleClickStrategy = ClojureInvoker.newInvoker(CCWPlugin.getDefault(),
"ccw.editors.clojure.double-click-strategy");
-
+
+ /** The Clojure editor */
+ private final IClojureEditor clojureEditor;
+
public ClojureSourceViewerConfiguration(IPreferenceStore preferenceStore,
IClojureEditor editor) {
super(preferenceStore, editor);
+ clojureEditor = editor;
}
@Override
@@ -126,10 +135,8 @@ public IInformationControl createInformationControl(Shell parent) {
@Override
public IReconciler getReconciler(ISourceViewer sourceViewer) {
- // cf. http://code.google.com/p/counterclockwise/issues/detail?id=5
- // Completely disable spellchecking until we can distinguish comments
- // and code, and spell check based on these partitions
- return null;
+ CompositeReconcilingStrategy strategy = new ClojureCompositeReconcilingStrategy(clojureEditor);
+ return new ClojureReconciler(clojureEditor, strategy, false);
}
@Override
diff --git a/ccw.core/src/java/ccw/editors/clojure/IClojureEditor.java b/ccw.core/src/java/ccw/editors/clojure/IClojureEditor.java
index eee4b8bd..09736323 100644
--- a/ccw.core/src/java/ccw/editors/clojure/IClojureEditor.java
+++ b/ccw.core/src/java/ccw/editors/clojure/IClojureEditor.java
@@ -6,6 +6,7 @@
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.source.DefaultCharacterPairMatcher;
+import org.eclipse.jface.text.source.projection.ProjectionAnnotationModel;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.ui.texteditor.AbstractTextEditor;
@@ -123,4 +124,16 @@ public interface IClojureEditor extends IAdaptable {
* @return The top level sexp.
*/
@Nullable String getTopLevelSExpression(final int caretOffset);
+
+ /**
+ * Get the annotation model for this editor.
+ * @return Returns the model or null
+ */
+ @Nullable ProjectionAnnotationModel getProjectionAnnotationModel();
+
+ /**
+ * Toggle projection (the base of the folding feature) on this IClojureEditor.
+ * @param enabled if true, it will enable the projection
+ */
+ void enableProjection(boolean enabled);
}
diff --git a/ccw.core/src/java/ccw/editors/clojure/SimpleSourceViewerConfiguration.java b/ccw.core/src/java/ccw/editors/clojure/SimpleSourceViewerConfiguration.java
index 1b09b081..f1886859 100644
--- a/ccw.core/src/java/ccw/editors/clojure/SimpleSourceViewerConfiguration.java
+++ b/ccw.core/src/java/ccw/editors/clojure/SimpleSourceViewerConfiguration.java
@@ -30,7 +30,9 @@
import org.eclipse.ui.editors.text.TextSourceViewerConfiguration;
import ccw.CCWPlugin;
+import ccw.editors.clojure.scanners.ClojurePartitionScanner;
import ccw.editors.clojure.scanners.ClojureTokenScanner;
+import ccw.editors.clojure.text.ClojureTopLevelFormsDamager;
import ccw.util.Pair;
/**
@@ -84,6 +86,7 @@ protected void addDamagerRepairerForContentType(PresentationReconciler reconcile
IPresentationDamager d = new ClojureTopLevelFormsDamager(editor);
reconciler.setDamager(d, contentType);
+ // AR - Each scanner has the Default Repairer at the moment
for (ITokenScanner scanner : tokenScanners) {
IPresentationRepairer r = new DefaultDamagerRepairer(scanner);
reconciler.setRepairer(r, contentType);
@@ -108,7 +111,7 @@ public String getConfiguredDocumentPartitioning(ISourceViewer sourceViewer) {
@Override
public String[] getConfiguredContentTypes(ISourceViewer sourceViewer) {
- return new String[] { IDocument.DEFAULT_CONTENT_TYPE };
+ return ClojurePartitionScanner.DEFAULT_CONTENT_TYPES;
}
@Override
diff --git a/ccw.core/src/java/ccw/editors/clojure/folding/FoldingActionGroup.java b/ccw.core/src/java/ccw/editors/clojure/folding/FoldingActionGroup.java
new file mode 100644
index 00000000..39af1879
--- /dev/null
+++ b/ccw.core/src/java/ccw/editors/clojure/folding/FoldingActionGroup.java
@@ -0,0 +1,229 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Laurent Petit.
+ * 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:
+ * IBM Corporation - initial API and implementation
+ * Andrea RICHIARDI - CCW adjustments
+ *******************************************************************************/
+package ccw.editors.clojure.folding;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jface.action.IAction;
+import org.eclipse.jface.action.IMenuManager;
+import org.eclipse.jface.preference.IPreferenceStore;
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.source.projection.IProjectionListener;
+import org.eclipse.jface.text.source.projection.ProjectionViewer;
+import org.eclipse.jface.util.IPropertyChangeListener;
+import org.eclipse.jface.util.PropertyChangeEvent;
+import org.eclipse.ui.actions.ActionGroup;
+import org.eclipse.ui.editors.text.IFoldingCommandIds;
+import org.eclipse.ui.texteditor.ITextEditor;
+import org.eclipse.ui.texteditor.ResourceAction;
+import org.eclipse.ui.texteditor.TextOperationAction;
+
+import ccw.CCWPlugin;
+import ccw.editors.clojure.IClojureEditor;
+import ccw.preferences.PreferenceConstants;
+import ccw.util.ClojureInvoker;
+
+/**
+ * Groups the JDT folding actions (from org.eclipse.jdt).
+ */
+public class FoldingActionGroup extends ActionGroup {
+
+ private final ClojureInvoker foldingSupport = ClojureInvoker.newInvoker(
+ CCWPlugin.getDefault(),
+ "ccw.editors.clojure.folding-support");
+
+ // private class FoldingAction extends PreferenceAction {
+ //
+ // FoldingAction(ResourceBundle bundle, String prefix) {
+ // super(bundle, prefix, IAction.AS_PUSH_BUTTON);
+ // }
+ //
+ // @Override
+ // public void update() {
+ // setEnabled(FoldingActionGroup.this.isEnabled() && fViewer.isProjectionMode());
+ // }
+ //
+ // }
+
+ private final @NonNull ITextEditor fEditor;
+ private final @NonNull ProjectionViewer fViewer;
+ private final @NonNull IPreferenceStore fPreferenceStore;
+
+ private final ResourceAction fToggle;
+ private final TextOperationAction fExpand;
+ private final TextOperationAction fCollapse;
+ private final TextOperationAction fExpandAll;
+ private final TextOperationAction fCollapseAll;
+
+ // TODO private final PreferenceAction fRestoreDefaults;
+ // TODO private final FoldingAction fCollapseComments;
+
+ private final IProjectionListener fProjectionListener;
+ private final IPropertyChangeListener fPreferenceListener;
+
+ /**
+ * Creates a new projection action group for editor. If the
+ * supplied viewer is not an instance of ProjectionViewer, the
+ * action group is disabled.
+ *
+ * @param editor the text editor to operate on
+ * @param viewer the viewer of the editor
+ */
+ public FoldingActionGroup(@NonNull ITextEditor editor, @NonNull ITextViewer viewer, @NonNull IPreferenceStore store) {
+ fEditor = editor;
+ fViewer = (ProjectionViewer) viewer;
+ fPreferenceStore = store;
+
+ fToggle = new ResourceAction(FoldingMessages.getResourceBundle(), "Projection_Toggle_", IAction.AS_CHECK_BOX) { //$NON-NLS-1$
+ @Override
+ public void run() {
+ if (fEditor instanceof IClojureEditor) {
+ IPreferenceStore store = CCWPlugin.getDefault().getPreferenceStore();
+ boolean current = store.getBoolean(PreferenceConstants.EDITOR_FOLDING_PROJECTION_ENABLED);
+ store.setValue(PreferenceConstants.EDITOR_FOLDING_PROJECTION_ENABLED, !current);
+ }
+ }
+ };
+
+ fToggle.setActionDefinitionId(IFoldingCommandIds.FOLDING_TOGGLE);
+ editor.setAction("FoldingToggle", fToggle); //$NON-NLS-1$
+
+ fExpandAll= new TextOperationAction(FoldingMessages.getResourceBundle(), "Projection_ExpandAll_", editor, ProjectionViewer.EXPAND_ALL, true); //$NON-NLS-1$
+ fExpandAll.setActionDefinitionId(IFoldingCommandIds.FOLDING_EXPAND_ALL);
+ editor.setAction("FoldingExpandAll", fExpandAll); //$NON-NLS-1$
+
+ fCollapseAll= new TextOperationAction(FoldingMessages.getResourceBundle(), "Projection_CollapseAll_", editor, ProjectionViewer.COLLAPSE_ALL, true); //$NON-NLS-1$
+ fCollapseAll.setActionDefinitionId(IFoldingCommandIds.FOLDING_COLLAPSE_ALL);
+ editor.setAction("FoldingCollapseAll", fCollapseAll); //$NON-NLS-1$
+
+ fExpand= new TextOperationAction(FoldingMessages.getResourceBundle(), "Projection_Expand_", editor, ProjectionViewer.EXPAND, true); //$NON-NLS-1$
+ fExpand.setActionDefinitionId(IFoldingCommandIds.FOLDING_EXPAND);
+ editor.setAction("FoldingExpand", fExpand); //$NON-NLS-1$
+
+ fCollapse= new TextOperationAction(FoldingMessages.getResourceBundle(), "Projection_Collapse_", editor, ProjectionViewer.COLLAPSE, true); //$NON-NLS-1$
+ fCollapse.setActionDefinitionId(IFoldingCommandIds.FOLDING_COLLAPSE);
+ editor.setAction("FoldingCollapse", fCollapse); //$NON-NLS-1$
+
+// fRestoreDefaults= new FoldingAction(FoldingMessages.getResourceBundle(), "Projection_Restore_") { //$NON-NLS-1$
+// @Override
+// public void run() {
+// if (editor instanceof JavaEditor) {
+// JavaEditor javaEditor= (JavaEditor) editor;
+// javaEditor.resetProjection();
+// }
+// }
+// };
+// fRestoreDefaults.setActionDefinitionId(IFoldingCommandIds.FOLDING_RESTORE);
+// editor.setAction("FoldingRestore", fRestoreDefaults); //$NON-NLS-1$
+//
+// fCollapseComments= new FoldingAction(FoldingMessages.getResourceBundle(), "Projection_CollapseComments_") { //$NON-NLS-1$
+// @Override
+// public void run() {
+// if (editor instanceof JavaEditor) {
+// JavaEditor javaEditor= (JavaEditor) editor;
+// javaEditor.collapseComments();
+// }
+// }
+// };
+// fCollapseComments.setActionDefinitionId(IJavaEditorActionDefinitionIds.FOLDING_COLLAPSE_COMMENTS);
+// editor.setAction("FoldingCollapseComments", fCollapseComments); //$NON-NLS-1$
+
+ fProjectionListener = new IProjectionListener() {
+ @Override
+ public void projectionEnabled() {
+ update();
+ }
+
+ @Override
+ public void projectionDisabled() {
+ update();
+ }
+ };
+ fViewer.addProjectionListener(fProjectionListener);
+
+ fPreferenceListener = new IPropertyChangeListener() {
+ @Override
+ public void propertyChange(PropertyChangeEvent event) {
+ if (event.getProperty().equals(PreferenceConstants.EDITOR_FOLDING_PROJECTION_ENABLED)) {
+ update();
+ }
+ }
+ };
+ fPreferenceStore.addPropertyChangeListener(fPreferenceListener);
+ }
+
+
+ private boolean projectionEnabled() {
+ return CCWPlugin.getDefault().getPreferenceStore()
+ .getBoolean(PreferenceConstants.EDITOR_FOLDING_PROJECTION_ENABLED);
+ }
+
+ private boolean canProjectAndFold(ITextEditor editor) {
+ Boolean descriptorsEnabled = (Boolean) foldingSupport._("any-descriptor-enabled?");
+ return (descriptorsEnabled && projectionEnabled());
+ }
+
+ /*
+ * @see org.eclipse.ui.actions.ActionGroup#dispose()
+ */
+ @Override
+ public void dispose() {
+ fViewer.removeProjectionListener(fProjectionListener);
+ fPreferenceStore.removePropertyChangeListener(fPreferenceListener);
+ super.dispose();
+ }
+
+ /**
+ * Updates the actions if the group is enabled.
+ */
+ protected void update() {
+ boolean canProjectAndFold = canProjectAndFold(fEditor);
+
+ fToggle.setChecked(projectionEnabled());
+
+ fExpand.update();
+ fExpand.setEnabled(canProjectAndFold);
+ fExpandAll.update();
+ fExpandAll.setEnabled(canProjectAndFold);
+ fCollapse.update();
+ fCollapse.setEnabled(canProjectAndFold);
+ fCollapseAll.update();
+ fCollapseAll.setEnabled(canProjectAndFold);
+ // fRestoreDefaults.update();
+ // fCollapseComments.update();
+ }
+
+ /**
+ * Fills the menu with all folding actions.
+ *
+ * @param manager the menu manager for the folding submenu
+ */
+ public void fillMenu(IMenuManager manager) {
+ if (projectionEnabled()) {
+ update();
+ manager.add(fToggle);
+ manager.add(fExpandAll);
+ manager.add(fExpand);
+ manager.add(fCollapse);
+ manager.add(fCollapseAll);
+ // manager.add(fRestoreDefaults);
+ // manager.add(fCollapseComments);
+ }
+ }
+
+ /*
+ * @see org.eclipse.ui.actions.ActionGroup#updateActionBars()
+ */
+ @Override
+ public void updateActionBars() {
+ update();
+ }
+}
diff --git a/ccw.core/src/java/ccw/editors/clojure/folding/FoldingDescriptor.java b/ccw.core/src/java/ccw/editors/clojure/folding/FoldingDescriptor.java
new file mode 100644
index 00000000..d0107a80
--- /dev/null
+++ b/ccw.core/src/java/ccw/editors/clojure/folding/FoldingDescriptor.java
@@ -0,0 +1,108 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Laurent Petit.
+ * 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:
+ * Andrea RICHIARDI - initial implementation
+ *******************************************************************************/
+package ccw.editors.clojure.folding;
+
+import java.util.Set;
+
+import ccw.util.AbstractModelObject;
+
+/**
+ * Data structure to hold a (mutable) folding descriptor.
+ * @author Andrea Richiardi
+ */
+public class FoldingDescriptor extends AbstractModelObject {
+
+ private String id;
+ private String label;
+ private Boolean enabled;
+ private String description;
+ private Set tags;
+
+ public FoldingDescriptor(String id, String label, Boolean enabled, String description, Set tags) {
+ super();
+ this.id = id;
+ this.description = description;
+ this.label = label;
+ this.enabled = enabled;
+ this.tags = tags;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public String getLabel() {
+ return label;
+ }
+
+ public Set getTags() {
+ return tags;
+ }
+
+ public void setId(String id) {
+ firePropertyChange("id", this.id, this.id = id);
+ }
+
+ public void setDescription(String description) {
+ firePropertyChange("description", this.description, this.description = description);
+ }
+
+ public void setLabel(String label) {
+ firePropertyChange("label", this.label, this.label = label);
+ }
+
+ public void setTags(Set tags) {
+ firePropertyChange("tags", this.tags, this.tags = tags);
+ }
+
+ public Boolean isEnabled() {
+ return enabled;
+ }
+
+ public void setEnabled(Boolean enabled) {
+ firePropertyChange("enabled", this.enabled, this.enabled = enabled);
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((id == null) ? 0 : id.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ FoldingDescriptor other = (FoldingDescriptor) obj;
+ if (id == null) {
+ if (other.id != null)
+ return false;
+ } else if (!id.equals(other.id))
+ return false;
+ return true;
+ }
+
+ @Override
+ public String toString() {
+ return "{id: " + id + ", label: " + label + ", enabled: " + enabled +
+ ", description: " + description + ", tags: " + tags.toString() + "}";
+ }
+}
diff --git a/ccw.core/src/java/ccw/editors/clojure/folding/FoldingMessages.java b/ccw.core/src/java/ccw/editors/clojure/folding/FoldingMessages.java
new file mode 100644
index 00000000..22cc49fc
--- /dev/null
+++ b/ccw.core/src/java/ccw/editors/clojure/folding/FoldingMessages.java
@@ -0,0 +1,91 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Laurent Petit.
+ * 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:
+ * IBM Corporation - initial API and implementation
+ * Andrea RICHIARDI - CCW adjustments
+ *******************************************************************************/
+package ccw.editors.clojure.folding;
+
+import java.util.MissingResourceException;
+import java.util.ResourceBundle;
+
+import org.eclipse.osgi.util.NLS;
+
+/**
+ * Class that gives access to the folding messages resource bundle.
+ */
+public class FoldingMessages {
+
+ private static final String BUNDLE_NAME = "ccw.editors.clojure.folding.FoldingMessages"; //$NON-NLS-1$
+
+ private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME);
+
+ private FoldingMessages() {
+ // no instance
+ }
+
+ public static String Projection_Toggle_label;
+ public static String Projection_Toggle_tooltip;
+ public static String Projection_Toggle_description;
+ public static String Projection_Toggle_image;
+ public static String Projection_ExpandAll_label;
+ public static String Projection_ExpandAll_tooltip;
+ public static String Projection_ExpandAll_description;
+ public static String Projection_ExpandAll_image;
+ public static String Projection_Expand_label;
+ public static String Projection_Expand_tooltip;
+ public static String Projection_Expand_description;
+ public static String Projection_Expand_image;
+ public static String Projection_CollapseAll_label;
+ public static String Projection_CollapseAll_tooltip;
+ public static String Projection_CollapseAll_description;
+ public static String Projection_CollapseAll_image;
+ public static String Projection_Collapse_label;
+ public static String Projection_Collapse_tooltip;
+ public static String Projection_Collapse_description;
+ public static String Projection_Collapse_image;
+ public static String Projection_FoldingMenu_name;
+
+// public static String Projection_Restore_label;
+// public static String Projection_Restore_tooltip;
+// public static String Projection_Restore_description;
+// public static String Projection_Restore_image;
+// public static String Projection_CollapseComments_label;
+// public static String Projection_CollapseComments_tooltip;
+// public static String Projection_CollapseComments_description;
+// public static String Projection_CollapseComments_image;
+
+ /**
+ * Returns the resource string associated with the given key in the resource bundle. If there isn't
+ * any value under the given key, the key is returned.
+ *
+ * @param key the resource key
+ * @return the string
+ */
+ public static String getString(String key) {
+ try {
+ return RESOURCE_BUNDLE.getString(key);
+ } catch (MissingResourceException e) {
+ return '!' + key + '!';
+ }
+ }
+
+ /**
+ * Returns the resource bundle managed by the receiver.
+ *
+ * @return the resource bundle
+ * @since 3.0
+ */
+ public static ResourceBundle getResourceBundle() {
+ return RESOURCE_BUNDLE;
+ }
+
+ static {
+ NLS.initializeMessages(BUNDLE_NAME, FoldingMessages.class);
+ }
+}
diff --git a/ccw.core/src/java/ccw/editors/clojure/folding/FoldingMessages.properties b/ccw.core/src/java/ccw/editors/clojure/folding/FoldingMessages.properties
new file mode 100644
index 00000000..5ff4ef4c
--- /dev/null
+++ b/ccw.core/src/java/ccw/editors/clojure/folding/FoldingMessages.properties
@@ -0,0 +1,47 @@
+###############################################################################
+# Copyright (c) 2015 Laurent Petit_
+# 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:
+# IBM Corporation - initial API and implementation
+# Andrea RICHIARDI - CCW adjustments
+###############################################################################
+Projection_Toggle_label= &Enable Folding
+Projection_Toggle_tooltip= Toggles Folding
+Projection_Toggle_description= Toggles folding for the current editor
+Projection_Toggle_image=
+
+Projection_ExpandAll_label= Expand &All
+Projection_ExpandAll_tooltip= Expands All Collapsed Regions
+Projection_ExpandAll_description= Expands any collapsed regions in the current editor
+Projection_ExpandAll_image=
+
+Projection_Expand_label= E&xpand
+Projection_Expand_tooltip= Expands the Current Collapsed Region
+Projection_Expand_description= Expands the collapsed region at the current selection
+Projection_Expand_image=
+
+Projection_CollapseAll_label= Collapse A&ll
+Projection_CollapseAll_tooltip= Collapses All Expanded Regions
+Projection_CollapseAll_description= Collapse any expanded regions in the current editor
+Projection_CollapseAll_image=
+
+Projection_Collapse_label= Colla&pse
+Projection_Collapse_tooltip= Collapses the Current Region
+Projection_Collapse_description= Collapses the Current Region
+Projection_Collapse_image=
+
+Projection_FoldingMenu_name=F&olding
+
+#Projection_Restore_label= &Reset Structure
+#Projection_Restore_tooltip= Restore the Original Folding Structure
+#Projection_Restore_description= Restores the original folding structure
+#Projection_Restore_image=
+
+#Projection_CollapseComments_label= Collapse &Comments
+#Projection_CollapseComments_tooltip= Collapses All Comments
+#Projection_CollapseComments_description= Collapses all comments
+#Projection_CollapseComments_image=
diff --git a/ccw.core/src/java/ccw/editors/clojure/folding/FoldingModel.java b/ccw.core/src/java/ccw/editors/clojure/folding/FoldingModel.java
new file mode 100644
index 00000000..bf20c7b3
--- /dev/null
+++ b/ccw.core/src/java/ccw/editors/clojure/folding/FoldingModel.java
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Laurent Petit.
+ * 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:
+ * Andrea RICHIARDI - initial implementation
+ *******************************************************************************/
+package ccw.editors.clojure.folding;
+
+import java.util.List;
+
+import org.eclipse.core.databinding.observable.list.IObservableList;
+
+/**
+ * The folding model. It is injected during the plugin initialization.
+ */
+public interface FoldingModel {
+
+ /**
+ * Retrieves a list of observable FoldingDescriptor(s).
+ * @return An ObservableList
+ */
+ IObservableList getObservableDescriptors();
+
+ /**
+ * Persists the input FoldingDescriptor(s).
+ */
+ void persistDescriptors(List descriptors);
+}
diff --git a/ccw.core/src/java/ccw/editors/clojure/scanners/ClojurePartitionScanner.java b/ccw.core/src/java/ccw/editors/clojure/scanners/ClojurePartitionScanner.java
index 2713cb6d..468f7cf3 100644
--- a/ccw.core/src/java/ccw/editors/clojure/scanners/ClojurePartitionScanner.java
+++ b/ccw.core/src/java/ccw/editors/clojure/scanners/ClojurePartitionScanner.java
@@ -8,20 +8,23 @@
* Contributors:
* Casey Marshall - initial API and implementation
* Laurent Petit - evolution and maintenance
+ * Andrea Richiardi - new ClojureCharRule
*******************************************************************************/
package ccw.editors.clojure.scanners;
import java.util.ArrayList;
import java.util.List;
+import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.rules.EndOfLineRule;
-import org.eclipse.jface.text.rules.ICharacterScanner;
import org.eclipse.jface.text.rules.IPredicateRule;
import org.eclipse.jface.text.rules.IToken;
import org.eclipse.jface.text.rules.MultiLineRule;
import org.eclipse.jface.text.rules.RuleBasedPartitionScanner;
import org.eclipse.jface.text.rules.Token;
+import ccw.editors.clojure.scanners.rules.ClojureCharRule;
+
public class ClojurePartitionScanner extends RuleBasedPartitionScanner {
public static final String CLOJURE_PARTITIONING = "__clojure_partitioning";
@@ -32,6 +35,9 @@ public class ClojurePartitionScanner extends RuleBasedPartitionScanner {
public final static String[] CLOJURE_CONTENT_TYPES=
new String[] { CLOJURE_COMMENT, CLOJURE_STRING, CLOJURE_CHAR };
+ public final static String[] DEFAULT_CONTENT_TYPES=
+ new String[] { IDocument.DEFAULT_CONTENT_TYPE };
+
public ClojurePartitionScanner() {
IToken comment = new Token(CLOJURE_COMMENT);
@@ -46,67 +52,4 @@ public ClojurePartitionScanner() {
setPredicateRules(rules.toArray(new IPredicateRule[rules.size()]));
}
-
- public static class ClojureCharRule implements IPredicateRule {
- private final IToken tokenToReturn;
- public ClojureCharRule(IToken tokenToReturn) {
- this.tokenToReturn = tokenToReturn;
- }
-
- public IToken evaluate(ICharacterScanner scanner, boolean resume) {
- if (resume == true) {
- throw new IllegalArgumentException("unhandled case when resume = true");
- } else {
- return evaluate(scanner);
- }
- }
-
- public IToken getSuccessToken() {
- return tokenToReturn;
- }
-
- public IToken evaluate(ICharacterScanner scanner) {
- int firstChar = scanner.read();
- if ((char)firstChar != '\\') {
- scanner.unread();
- return Token.UNDEFINED;
- } else {
- int next = scanner.read();
- if (Character.isWhitespace(next)) {
- scanner.unread();
- return Token.UNDEFINED;
- }
-// if ((char)next == 'n') {
-// int second = scanner.read();
-// if ((char)second != 'e') {
-// scanner.unread();
-// return getSuccessToken();
-// } else {
-//
-// }
-// }
-// if ((char)next == 's') {
-// int second = scanner.read();
-// if ((char)second != 'p') {
-// scanner.unread();
-// return getSuccessToken();
-// } else {
-//
-// }
-// }
-// if ((char)next == 't') {
-// int second = scanner.read();
-// if ((char)second != 'a') {
-// scanner.unread();
-// return getSuccessToken();
-// } else {
-//
-// }
-// }
- return getSuccessToken();
- }
- }
-
- }
-
}
diff --git a/ccw.core/src/java/ccw/editors/clojure/scanners/ClojureTokenScanner.java b/ccw.core/src/java/ccw/editors/clojure/scanners/ClojureTokenScanner.java
index fb1952eb..f0b84661 100644
--- a/ccw.core/src/java/ccw/editors/clojure/scanners/ClojureTokenScanner.java
+++ b/ccw.core/src/java/ccw/editors/clojure/scanners/ClojureTokenScanner.java
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2008 Laurent Petit.
+ * Copyright (c) 2015 Laurent Petit.
* 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
@@ -7,10 +7,26 @@
*
* Contributors:
* Laurent PETIT - initial API and implementation
- * Thomas Ettinger
+ * Thomas ETTINGER
+ * Andrea RICHIARDI - refactored out parts to TokenScannerUtils
*******************************************************************************/
package ccw.editors.clojure.scanners;
+import static ccw.editors.clojure.scanners.TokenScannerUtils.closeChimeraKeyword;
+import static ccw.editors.clojure.scanners.TokenScannerUtils.closeFnKeyword;
+import static ccw.editors.clojure.scanners.TokenScannerUtils.closeListKeyword;
+import static ccw.editors.clojure.scanners.TokenScannerUtils.metaKeyword;
+import static ccw.editors.clojure.scanners.TokenScannerUtils.nestKeyword;
+import static ccw.editors.clojure.scanners.TokenScannerUtils.openChimeraKeyword;
+import static ccw.editors.clojure.scanners.TokenScannerUtils.openFnKeyword;
+import static ccw.editors.clojure.scanners.TokenScannerUtils.openListKeyword;
+import static ccw.editors.clojure.scanners.TokenScannerUtils.readerLiteralTagKeyword;
+import static ccw.editors.clojure.scanners.TokenScannerUtils.symbolKeyword;
+import static ccw.editors.clojure.scanners.TokenScannerUtils.tokenLengthKeyword;
+import static ccw.editors.clojure.scanners.TokenScannerUtils.tokenTypeKeyword;
+import static ccw.editors.clojure.scanners.TokenScannerUtils.unnestKeyword;
+import static ccw.editors.clojure.scanners.TokenScannerUtils.whitespaceKeyword;
+
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@@ -45,12 +61,12 @@ public final class ClojureTokenScanner implements ITokenScanner, IPropertyChange
private final ClojureInvoker topLevelFormsDamager = ClojureInvoker.newInvoker(
CCWPlugin.getDefault(),
"ccw.editors.clojure.ClojureTopLevelFormsDamagerImpl");
-
+
private int currentOffset;
private final Map parserTokenKeywordToJFaceToken;
private String text;
private final IScanContext context;
-
+
private final Keyword[] parenLevelPrefKeywords = new Keyword[] {
PreferenceConstants.rainbowParenLevel1,
PreferenceConstants.rainbowParenLevel2,
@@ -66,27 +82,12 @@ public final class ClojureTokenScanner implements ITokenScanner, IPropertyChange
private IClojureEditor clojureEditor;
private IPreferenceStore preferenceStore;
- private TokenScannerUtils utils;
-
+
protected static final IToken errorToken = new org.eclipse.jface.text.rules.Token(new TextAttribute(Display.getDefault().getSystemColor(SWT.COLOR_WHITE), Display.getDefault().getSystemColor(SWT.COLOR_DARK_RED), TextAttribute.UNDERLINE));
private int currentParenLevel = 0;
private ISeq tokenSeq;
private Map,?> currentToken;
- private static Keyword symbolKeyword = Keyword.intern("symbol");
- private static Keyword tokenTypeKeyword = Keyword.intern("token-type");
- private static Keyword tokenLengthKeyword = Keyword.intern("token-length");
- private static Keyword nestKeyword = Keyword.intern("nest");
- private static Keyword unnestKeyword = Keyword.intern("unnest");
- private static Keyword openListKeyword = Keyword.intern("open-list");
- private static Keyword openFnKeyword = Keyword.intern("open-fn");
- private static Keyword openChimeraKeyword = Keyword.intern("open-chimera");
- private static Keyword closeListKeyword = Keyword.intern("close-list");
- private static Keyword closeFnKeyword = Keyword.intern("close-fn");
- private static Keyword closeChimeraKeyword = Keyword.intern("close-chimera");
- private static Keyword metaKeyword = Keyword.intern("meta");
- private static Keyword readerLiteralTagKeyword = Keyword.intern("reader-literal");
- private static Keyword whitespaceKeyword = Keyword.intern("whitespace");
-
+
public ClojureTokenScanner(IScanContext context, IPreferenceStore preferenceStore, IClojureEditor clojureEditor) {
Assert.assertNotNull(clojureEditor);
@@ -94,24 +95,23 @@ public ClojureTokenScanner(IScanContext context, IPreferenceStore preferenceStor
this.preferenceStore = preferenceStore;
this.clojureEditor = clojureEditor;
parserTokenKeywordToJFaceToken = new HashMap();
- utils = new TokenScannerUtils(this);
- initClojureTokenTypeToJFaceTokenMap(utils);
+ initClojureTokenTypeToJFaceTokenMap();
}
- protected void initClojureTokenTypeToJFaceTokenMap(TokenScannerUtils u) {
- u.addTokenType(Keyword.intern("unexpected"), ClojureTokenScanner.errorToken);
- u.addTokenType(Keyword.intern("eof"), Token.EOF);
- u.addTokenType(Keyword.intern("whitespace"), Token.WHITESPACE);
+ protected void initClojureTokenTypeToJFaceTokenMap() {
+ TokenScannerUtils.addTokenType(this, Keyword.intern("unexpected"), ClojureTokenScanner.errorToken);
+ TokenScannerUtils.addTokenType(this, Keyword.intern("eof"), Token.EOF);
+ TokenScannerUtils.addTokenType(this, Keyword.intern("whitespace"), Token.WHITESPACE);
- for (Keyword token: PreferenceConstants.colorizableTokens) {
- PreferenceConstants.ColorizableToken tokenStyle = PreferenceConstants.getColorizableToken(preferenceStore, token);
- u.addTokenType(
- token,
- StringConverter.asString(tokenStyle.rgb),
- tokenStyle.isBold,
- tokenStyle.isItalic);
- }
- }
+ for (Keyword token: PreferenceConstants.colorizableTokens) {
+ PreferenceConstants.ColorizableToken tokenStyle = PreferenceConstants.getColorizableToken(preferenceStore, token);
+ TokenScannerUtils.addTokenType(this,
+ token,
+ StringConverter.asString(tokenStyle.rgb),
+ tokenStyle.isBold,
+ tokenStyle.isItalic);
+ }
+ }
public final void addTokenType(Keyword tokenIndex, org.eclipse.jface.text.rules.IToken token) {
@@ -355,7 +355,7 @@ public void propertyChange(PropertyChangeEvent event) {
Keyword keyword = PreferenceConstants.guessPreferenceKeyword(event.getProperty());
if (PreferenceConstants.colorizableTokens.contains(keyword)) {
IToken eclipseToken = parserTokenKeywordToJFaceToken.get(keyword);
- utils.addTokenType(keyword, adaptToken(eclipseToken, event.getProperty(), event.getNewValue()));
+ TokenScannerUtils.addTokenType(this, keyword, adaptToken(eclipseToken, event.getProperty(), event.getNewValue()));
clojureEditor.markDamagedAndRedraw();
}
}
diff --git a/ccw.core/src/java/ccw/editors/clojure/scanners/TokenScannerUtils.java b/ccw.core/src/java/ccw/editors/clojure/scanners/TokenScannerUtils.java
index ed8d4876..82d4211f 100644
--- a/ccw.core/src/java/ccw/editors/clojure/scanners/TokenScannerUtils.java
+++ b/ccw.core/src/java/ccw/editors/clojure/scanners/TokenScannerUtils.java
@@ -1,5 +1,5 @@
/*******************************************************************************
- * Copyright (c) 2008 Laurent Petit.
+ * Copyright (c) 2015 Laurent Petit.
* 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
@@ -7,11 +7,14 @@
*
* Contributors:
* Laurent PETIT - initial API and implementation
+ * Andrea RICHIARDI - removed state and pasted useful keywords
+ * added readToken for ClojureCharRule
*******************************************************************************/
package ccw.editors.clojure.scanners;
import org.eclipse.jface.resource.StringConverter;
import org.eclipse.jface.text.TextAttribute;
+import org.eclipse.jface.text.rules.ICharacterScanner;
import org.eclipse.jface.text.rules.IToken;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.RGB;
@@ -19,15 +22,28 @@
import ccw.CCWPlugin;
import clojure.lang.Keyword;
-
+/**
+ * Common utils for scanning tokens using Paredit's parse tree
+ */
public class TokenScannerUtils {
- private ClojureTokenScanner scanner;
-// private ColorRegistry colorCache;
+
+ public static Keyword symbolKeyword = Keyword.intern("symbol");
+ public static Keyword tokenTypeKeyword = Keyword.intern("token-type");
+ public static Keyword tokenLengthKeyword = Keyword.intern("token-length");
+ public static Keyword nestKeyword = Keyword.intern("nest");
+ public static Keyword unnestKeyword = Keyword.intern("unnest");
+ public static Keyword openListKeyword = Keyword.intern("open-list");
+ public static Keyword openFnKeyword = Keyword.intern("open-fn");
+ public static Keyword openChimeraKeyword = Keyword.intern("open-chimera");
+ public static Keyword closeListKeyword = Keyword.intern("close-list");
+ public static Keyword closeFnKeyword = Keyword.intern("close-fn");
+ public static Keyword closeChimeraKeyword = Keyword.intern("close-chimera");
+ public static Keyword metaKeyword = Keyword.intern("meta");
+ public static Keyword readerLiteralTagKeyword = Keyword.intern("reader-literal");
+ public static Keyword whitespaceKeyword = Keyword.intern("whitespace");
+
+ private TokenScannerUtils() {}
- public TokenScannerUtils(ClojureTokenScanner scanner) {
- this.scanner = scanner;
- }
-
/**
* Creates the data to feed an IToken.
* @param rgb A color (RGB).
@@ -69,11 +85,117 @@ public static TextAttribute createTokenData(String rgb, Boolean isBold, Boolean
* @param isBold
* @param isItalic
*/
- public void addTokenType(Keyword tokenIndex, String rgb, Boolean isBold, Boolean isItalic) {
+ public static void addTokenType(ClojureTokenScanner scanner, Keyword tokenIndex, String rgb, Boolean isBold, Boolean isItalic) {
scanner.addTokenType(tokenIndex, createTokenData(rgb, isBold, isItalic));
}
- public void addTokenType(Keyword tokenIndex, IToken token) {
+ public static void addTokenType(ClojureTokenScanner scanner, Keyword tokenIndex, IToken token) {
scanner.addTokenType(tokenIndex, token);
}
+
+ /**
+ * Reads a token, implementation adapted from
+ * Clojure.
+ * @param scanner A ICharacterScanner
+ * @param sb An already built StringBuilder
+ * @return A token string
+ */
+ private static String readToken(ICharacterScanner scanner, StringBuilder sb) {
+ for(; ;)
+ {
+ int ch = scanner.read();
+ if(ch == -1 || isWhitespace(ch) || isTerminatingMacro(ch))
+ {
+ scanner.unread();
+ return sb.toString();
+ }
+ sb.append((char) ch);
+ }
+ }
+
+ /**
+ * Reads a token, implementation adapted from
+ * Clojure.
+ * @param scanner A ICharacterScanner
+ * @return A token string
+ */
+ public static String readToken(ICharacterScanner scanner) {
+ StringBuilder sb = new StringBuilder();
+ return readToken(scanner, sb);
+ }
+
+ /**
+ * Reads a token, implementation adapted from
+ * Clojure.
+ * @param scanner A ICharacterScanner
+ * @param initch Init char
+ * @return A token string
+ */
+ public static String readToken(ICharacterScanner scanner, char initch) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(initch);
+ return readToken(scanner, sb);
+ }
+
+ /**
+ * Reads a Unicode char from a token, implementation adapted from
+ * Clojure.
+ * @param token The token
+ * @param offset An offset in the token
+ * @param length The length of the token
+ * @param base the base used for the decoding
+ * @return
+ */
+ public static int readUnicodeChar(String token, int offset, int length, int base) {
+ if(token.length() != offset + length)
+ throw new IllegalArgumentException("Invalid unicode character: \\" + token);
+ int uc = 0;
+ for(int i = offset; i < offset + length; ++i)
+ {
+ int d = Character.digit(token.charAt(i), base);
+ if(d == -1)
+ throw new IllegalArgumentException("Invalid digit: " + token.charAt(i));
+ uc = uc * base + d;
+ }
+ return (char) uc;
+ }
+
+ /**
+ * AR - Could have used paredit.clj/loc_utils definition of whitespace here but I guess this is faster.
+ * @param ch
+ * @return
+ */
+ public static boolean isWhitespace(int ch){
+ return Character.isWhitespace(ch) || ch == ',';
+ }
+
+ // AR - I am leaving the structure of clojure's source as it might be still useful
+ public static Character[] macroChars = new Character[256];
+ static {
+ macroChars['"'] = new Character('"');
+ macroChars[';'] = new Character(';');
+ macroChars['\''] = new Character('\'');
+ macroChars['@'] = new Character('@');
+ macroChars['^'] = new Character('^');
+ macroChars['`'] = new Character('`');
+ macroChars['~'] = new Character('~');
+ macroChars['('] = new Character('(');
+ macroChars[')'] = new Character(')');
+ macroChars['['] = new Character('[');
+ macroChars[']'] = new Character(']');
+ macroChars['{'] = new Character('{');
+ macroChars['}'] = new Character('}');
+ // macroChars['|'] = new Character('|');
+ macroChars['\\'] = new Character('\\');
+ macroChars['%'] = new Character('%');
+ macroChars['#'] = new Character('#');
+ }
+
+ static private boolean isMacro(int ch){
+ return (ch < macroChars.length && macroChars[ch] != null);
+ }
+
+ static private boolean isTerminatingMacro(int ch){
+ return (ch != '#' && ch != '\'' && ch != '%' && isMacro(ch));
+ }
}
diff --git a/ccw.core/src/java/ccw/editors/clojure/scanners/ClojurePartitioner.java b/ccw.core/src/java/ccw/editors/clojure/scanners/TracingPartitioner.java
similarity index 90%
rename from ccw.core/src/java/ccw/editors/clojure/scanners/ClojurePartitioner.java
rename to ccw.core/src/java/ccw/editors/clojure/scanners/TracingPartitioner.java
index 15cfc5e2..2b9ee91c 100644
--- a/ccw.core/src/java/ccw/editors/clojure/scanners/ClojurePartitioner.java
+++ b/ccw.core/src/java/ccw/editors/clojure/scanners/TracingPartitioner.java
@@ -20,9 +20,9 @@
import ccw.TraceOptions;
import ccw.util.ITracer;
-public class ClojurePartitioner extends FastPartitioner {
+public class TracingPartitioner extends FastPartitioner {
- public ClojurePartitioner(IPartitionTokenScanner scanner, String[] legalContentTypes) {
+ public TracingPartitioner(IPartitionTokenScanner scanner, String[] legalContentTypes) {
super(scanner, legalContentTypes);
}
@@ -36,7 +36,7 @@ public void connect(IDocument document, boolean delayInitialization) {
private void printPartitions(IDocument document) {
ITracer tracer = CCWPlugin.getTracer();
- if (tracer.isEnabled(TraceOptions.PARTITIONERS)) {
+ if (tracer.isEnabled(TraceOptions.EDITOR_SCANNERS)) {
StringBuffer buffer = new StringBuffer();
@@ -60,7 +60,7 @@ private void printPartitions(IDocument document) {
e.printStackTrace();
}
}
- tracer.trace(TraceOptions.PARTITIONERS, buffer);
+ tracer.trace(TraceOptions.EDITOR_SCANNERS, buffer);
}
}
}
diff --git a/ccw.core/src/java/ccw/editors/clojure/scanners/rules/ClojureCharRule.java b/ccw.core/src/java/ccw/editors/clojure/scanners/rules/ClojureCharRule.java
new file mode 100644
index 00000000..af22ed02
--- /dev/null
+++ b/ccw.core/src/java/ccw/editors/clojure/scanners/rules/ClojureCharRule.java
@@ -0,0 +1,92 @@
+package ccw.editors.clojure.scanners.rules;
+
+import org.eclipse.jface.text.rules.ICharacterScanner;
+import org.eclipse.jface.text.rules.IPredicateRule;
+import org.eclipse.jface.text.rules.IToken;
+import org.eclipse.jface.text.rules.Token;
+
+import ccw.editors.clojure.scanners.TokenScannerUtils;
+
+/**
+ * The Character rule, implementation adapted from
+ * Clojure.
+ * @author Andrea Richiardi
+ *
+ */
+public class ClojureCharRule implements IPredicateRule {
+ private final IToken tokenToReturn;
+ public ClojureCharRule(IToken tokenToReturn) {
+ this.tokenToReturn = tokenToReturn;
+ }
+
+ public IToken evaluate(ICharacterScanner scanner, boolean resume) {
+ if (resume == true) {
+ throw new IllegalArgumentException("unhandled case when resume = true");
+ } else {
+ return evaluate(scanner);
+ }
+ }
+
+ public IToken getSuccessToken() {
+ return tokenToReturn;
+ }
+
+ @Override
+ public IToken evaluate(ICharacterScanner scanner) {
+ int readCharCount = 0;
+
+ int ch = scanner.read();
+ readCharCount += 1;
+
+ if(ch == ICharacterScanner.EOF) return undefined(scanner, readCharCount);
+ if ((char)ch != '\\') return undefined(scanner, readCharCount);
+
+ String token = TokenScannerUtils.readToken(scanner);
+ readCharCount += token.length();
+
+ if(token.length() == 1) {
+ char next = token.charAt(0);
+ if (Character.isDefined(next)) return getSuccessToken();
+ else return undefined(scanner, readCharCount);
+ }
+ else if(token.equals("newline"))
+ return getSuccessToken();
+ else if(token.equals("space"))
+ return getSuccessToken();
+ else if(token.equals("tab"))
+ return getSuccessToken();
+ else if(token.equals("backspace"))
+ return getSuccessToken();
+ else if(token.equals("formfeed"))
+ return getSuccessToken();
+ else if(token.equals("return"))
+ return getSuccessToken();
+ else if(token.startsWith("u")) {
+ char c = (char) TokenScannerUtils.readUnicodeChar(token, 1, 4, 16);
+ if(c >= '\uD800' && c <= '\uDFFF') // surrogate code unit?
+ return undefined(scanner, readCharCount);
+ return getSuccessToken();
+ } else if(token.startsWith("o")) {
+ int len = token.length() - 1;
+ if(len > 3)
+ return undefined(scanner, readCharCount);
+ int uc = TokenScannerUtils.readUnicodeChar(token, 1, len, 8);
+ if(uc > 0377)
+ return undefined(scanner, readCharCount);
+ return getSuccessToken();
+ }
+ return undefined(scanner, readCharCount);
+ }
+
+ /**
+ * Returns Token.UNDEFINED and unreads a char.
+ * @param scanner
+ * @return Token.UNDEFINED
+ */
+ private IToken undefined(ICharacterScanner scanner, int charCountToUnread) {
+ for (int i = 0; i < charCountToUnread; i++) {
+ scanner.unread();
+ }
+ return Token.UNDEFINED;
+ }
+}
diff --git a/ccw.core/src/java/ccw/editors/clojure/text/ClojureCompositeReconcilingStrategy.java b/ccw.core/src/java/ccw/editors/clojure/text/ClojureCompositeReconcilingStrategy.java
new file mode 100644
index 00000000..1fb3f31f
--- /dev/null
+++ b/ccw.core/src/java/ccw/editors/clojure/text/ClojureCompositeReconcilingStrategy.java
@@ -0,0 +1,29 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Laurent Petit.
+ * 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:
+ * Andrea RICHIARDI - Initial implementation
+ *******************************************************************************/
+package ccw.editors.clojure.text;
+
+import org.eclipse.jface.text.reconciler.IReconcilingStrategy;
+
+import ccw.editors.clojure.IClojureEditor;
+
+/**
+ * A composite reconciling strategy for CCW's content types.
+ * Source of this pattern: {@link org.eclipse.jdt.internal.ui.text.CompositeReconcilingStrategy}
+ *
+ */
+public class ClojureCompositeReconcilingStrategy extends CompositeReconcilingStrategy{
+
+ public ClojureCompositeReconcilingStrategy(IClojureEditor editor) {
+ setReconcilingStrategies(new IReconcilingStrategy[] {
+ new FoldingReconcileStrategy(editor)
+ });
+ }
+}
diff --git a/ccw.core/src/java/ccw/editors/clojure/text/ClojureReconciler.java b/ccw.core/src/java/ccw/editors/clojure/text/ClojureReconciler.java
new file mode 100644
index 00000000..46b81b71
--- /dev/null
+++ b/ccw.core/src/java/ccw/editors/clojure/text/ClojureReconciler.java
@@ -0,0 +1,176 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Laurent Petit.
+ * 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:
+ * Andrea RICHIARDI - Initial creation
+ *******************************************************************************/
+package ccw.editors.clojure.text;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.eclipse.jface.text.ITextViewer;
+import org.eclipse.jface.text.reconciler.IReconcilingStrategy;
+import org.eclipse.jface.text.reconciler.MonoReconciler;
+import org.eclipse.swt.events.ShellAdapter;
+import org.eclipse.swt.events.ShellEvent;
+import org.eclipse.swt.events.ShellListener;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.ui.IPartListener;
+import org.eclipse.ui.IWorkbenchPart;
+import org.eclipse.ui.IWorkbenchPartSite;
+import org.eclipse.ui.IWorkbenchWindow;
+
+import ccw.editors.clojure.IClojureEditor;
+
+/**
+ * The Clojure Reconciler, which extends MonoReconciler and adds some part life-cycle logic
+ */
+public class ClojureReconciler extends MonoReconciler {
+
+ /** The reconciler's editor */
+ private IClojureEditor editor;
+ /** The part listener */
+ private IPartListener partListener;
+ /** The shell listener */
+ private ShellListener activationListener;
+
+ /** Is the editor active? */
+ private AtomicBoolean isEditorActive;
+
+ public ClojureReconciler(IClojureEditor editor, IReconcilingStrategy strategy, boolean isIncremental) {
+ super(strategy, isIncremental);
+ this.editor= editor;
+ isEditorActive = new AtomicBoolean();
+ }
+
+ @Override
+ protected synchronized void startReconciling() {
+ if (isEditorActive.get()) {
+ super.startReconciling();
+ }
+ }
+
+ @Override
+ public void install(ITextViewer textViewer) {
+ super.install(textViewer);
+
+ IWorkbenchPart part = (IWorkbenchPart) editor.getAdapter(IWorkbenchPart.class);
+ if (part != null) {
+ partListener= new PartListener();
+
+ IWorkbenchPartSite site = part.getSite();
+ IWorkbenchWindow window= site.getWorkbenchWindow();
+ window.getPartService().addPartListener(partListener);
+
+ activationListener = new ActivationListener(textViewer.getTextWidget());
+ Shell shell= window.getShell();
+ shell.addShellListener(activationListener);
+ }
+ }
+
+ @Override
+ public void uninstall() {
+ super.uninstall();
+
+ IWorkbenchPart part = (IWorkbenchPart) editor.getAdapter(IWorkbenchPart.class);
+ if (part != null) {
+ IWorkbenchPartSite site = part.getSite();
+ IWorkbenchWindow window= site.getWorkbenchWindow();
+ window.getPartService().removePartListener(partListener);
+ partListener= null;
+
+ Shell shell= window.getShell();
+ if (shell != null && !shell.isDisposed()) {
+ shell.removeShellListener(activationListener);
+ }
+ activationListener= null;
+ }
+ }
+
+ /**
+ * From org.eclipse.jdt, part listener for activating the reconciler.
+ */
+ private class PartListener implements IPartListener {
+
+ /*
+ * @see org.eclipse.ui.IPartListener#partActivated(org.eclipse.ui.IWorkbenchPart)
+ */
+ @Override
+ public void partActivated(IWorkbenchPart part) {
+ if (part == editor) {
+ isEditorActive.set(true);
+ startReconciling();
+ }
+ }
+
+ /*
+ * @see org.eclipse.ui.IPartListener#partBroughtToTop(org.eclipse.ui.IWorkbenchPart)
+ */
+ @Override
+ public void partBroughtToTop(IWorkbenchPart part) {
+ }
+
+ /*
+ * @see org.eclipse.ui.IPartListener#partClosed(org.eclipse.ui.IWorkbenchPart)
+ */
+ @Override
+ public void partClosed(IWorkbenchPart part) {
+ }
+
+ /*
+ * @see org.eclipse.ui.IPartListener#partDeactivated(org.eclipse.ui.IWorkbenchPart)
+ */
+ @Override
+ public void partDeactivated(IWorkbenchPart part) {
+ if (part == editor) {
+ isEditorActive.set(false);
+ }
+
+ }
+
+ /*
+ * @see org.eclipse.ui.IPartListener#partOpened(org.eclipse.ui.IWorkbenchPart)
+ */
+ @Override
+ public void partOpened(IWorkbenchPart part) {
+ }
+ }
+
+ /**
+ * From org.eclipse.jdt, shell activation listener for activating the reconciler.
+ */
+ private class ActivationListener extends ShellAdapter {
+
+ private Control fControl;
+
+ public ActivationListener(Control control) {
+ fControl= control;
+ }
+
+ /*
+ * @see org.eclipse.swt.events.ShellListener#shellActivated(org.eclipse.swt.events.ShellEvent)
+ */
+ @Override
+ public void shellActivated(ShellEvent e) {
+ if (!fControl.isDisposed() && fControl.isVisible()) {
+ isEditorActive.set(true);
+ startReconciling();
+ }
+ }
+
+ /*
+ * @see org.eclipse.swt.events.ShellListener#shellDeactivated(org.eclipse.swt.events.ShellEvent)
+ */
+ @Override
+ public void shellDeactivated(ShellEvent e) {
+ if (!fControl.isDisposed() && fControl.getShell() == e.getSource()) {
+ isEditorActive.set(false);
+ }
+ }
+ }
+}
diff --git a/ccw.core/src/java/ccw/editors/clojure/ClojureTopLevelFormsDamager.java b/ccw.core/src/java/ccw/editors/clojure/text/ClojureTopLevelFormsDamager.java
similarity index 90%
rename from ccw.core/src/java/ccw/editors/clojure/ClojureTopLevelFormsDamager.java
rename to ccw.core/src/java/ccw/editors/clojure/text/ClojureTopLevelFormsDamager.java
index 49e0d66f..d40e2ac6 100644
--- a/ccw.core/src/java/ccw/editors/clojure/ClojureTopLevelFormsDamager.java
+++ b/ccw.core/src/java/ccw/editors/clojure/text/ClojureTopLevelFormsDamager.java
@@ -1,4 +1,4 @@
-package ccw.editors.clojure;
+package ccw.editors.clojure.text;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument;
@@ -7,6 +7,7 @@
import org.eclipse.jface.text.presentation.IPresentationDamager;
import ccw.CCWPlugin;
+import ccw.editors.clojure.IClojureEditor;
import ccw.util.ClojureInvoker;
import clojure.lang.Ref;
diff --git a/ccw.core/src/java/ccw/editors/clojure/text/CompositeReconcilingStrategy.java b/ccw.core/src/java/ccw/editors/clojure/text/CompositeReconcilingStrategy.java
new file mode 100644
index 00000000..04ae145f
--- /dev/null
+++ b/ccw.core/src/java/ccw/editors/clojure/text/CompositeReconcilingStrategy.java
@@ -0,0 +1,112 @@
+package ccw.editors.clojure.text;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.reconciler.DirtyRegion;
+import org.eclipse.jface.text.reconciler.IReconcilingStrategy;
+import org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension;
+
+/**
+ * A reconciling strategy consisting of a sequence of internal reconciling strategies.
+ * By default, all requests are passed on to the contained strategies.
+ *
+ * @since 3.0
+ */
+public class CompositeReconcilingStrategy implements IReconcilingStrategy, IReconcilingStrategyExtension {
+
+ /** The list of internal reconciling strategies. */
+ private IReconcilingStrategy[] fStrategies;
+
+ /**
+ * Creates a new, empty composite reconciling strategy.
+ */
+ public CompositeReconcilingStrategy() {
+ }
+
+ /**
+ * Sets the reconciling strategies for this composite strategy.
+ *
+ * @param strategies the strategies to be set or null
+ */
+ public void setReconcilingStrategies(IReconcilingStrategy[] strategies) {
+ fStrategies= strategies;
+ }
+
+ /**
+ * Returns the previously set stratgies or null.
+ *
+ * @return the contained strategies or null
+ */
+ public IReconcilingStrategy[] getReconcilingStrategies() {
+ return fStrategies;
+ }
+
+ /*
+ * @see org.eclipse.jface.text.reconciler.IReconcilingStrategy#setDocument(org.eclipse.jface.text.IDocument)
+ */
+ @Override
+ public void setDocument(IDocument document) {
+ if (fStrategies == null)
+ return;
+
+ for (int i= 0; i < fStrategies.length; i++)
+ fStrategies[i].setDocument(document);
+ }
+
+ /*
+ * @see org.eclipse.jface.text.reconciler.IReconcilingStrategy#reconcile(org.eclipse.jface.text.reconciler.DirtyRegion, org.eclipse.jface.text.IRegion)
+ */
+ @Override
+ public void reconcile(DirtyRegion dirtyRegion, IRegion subRegion) {
+ if (fStrategies == null)
+ return;
+
+ for (int i= 0; i < fStrategies.length; i++)
+ fStrategies[i].reconcile(dirtyRegion, subRegion);
+ }
+
+ /*
+ * @see org.eclipse.jface.text.reconciler.IReconcilingStrategy#reconcile(org.eclipse.jface.text.IRegion)
+ */
+ @Override
+ public void reconcile(IRegion partition) {
+ if (fStrategies == null)
+ return;
+
+ for (int i= 0; i < fStrategies.length; i++)
+ fStrategies[i].reconcile(partition);
+ }
+
+ /*
+ * @see org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension#setProgressMonitor(org.eclipse.core.runtime.IProgressMonitor)
+ */
+ @Override
+ public void setProgressMonitor(IProgressMonitor monitor) {
+ if (fStrategies == null)
+ return;
+
+ for (int i=0; i < fStrategies.length; i++) {
+ if (fStrategies[i] instanceof IReconcilingStrategyExtension) {
+ IReconcilingStrategyExtension extension= (IReconcilingStrategyExtension) fStrategies[i];
+ extension.setProgressMonitor(monitor);
+ }
+ }
+ }
+
+ /*
+ * @see org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension#initialReconcile()
+ */
+ @Override
+ public void initialReconcile() {
+ if (fStrategies == null)
+ return;
+
+ for (int i=0; i < fStrategies.length; i++) {
+ if (fStrategies[i] instanceof IReconcilingStrategyExtension) {
+ IReconcilingStrategyExtension extension= (IReconcilingStrategyExtension) fStrategies[i];
+ extension.initialReconcile();
+ }
+ }
+ }
+}
diff --git a/ccw.core/src/java/ccw/editors/clojure/text/FoldingReconcileStrategy.java b/ccw.core/src/java/ccw/editors/clojure/text/FoldingReconcileStrategy.java
new file mode 100644
index 00000000..bb28e2aa
--- /dev/null
+++ b/ccw.core/src/java/ccw/editors/clojure/text/FoldingReconcileStrategy.java
@@ -0,0 +1,73 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Laurent Petit.
+ * 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:
+ * Andrea RICHIARDI - Initial creation
+ *******************************************************************************/
+package ccw.editors.clojure.text;
+
+import org.eclipse.core.runtime.IProgressMonitor;
+import org.eclipse.jface.text.IDocument;
+import org.eclipse.jface.text.IRegion;
+import org.eclipse.jface.text.reconciler.DirtyRegion;
+import org.eclipse.jface.text.reconciler.IReconcilingStrategy;
+import org.eclipse.jface.text.reconciler.IReconcilingStrategyExtension;
+
+import ccw.CCWPlugin;
+import ccw.TraceOptions;
+import ccw.editors.clojure.IClojureEditor;
+import ccw.util.ClojureInvoker;
+
+/**
+ * Clojure folding reconciling strategy
+ */
+public class FoldingReconcileStrategy implements IReconcilingStrategy, IReconcilingStrategyExtension {
+
+ private final ClojureInvoker foldingSupport = ClojureInvoker.newInvoker(
+ CCWPlugin.getDefault(),
+ "ccw.editors.clojure.folding-support");
+
+ /** The Clojure editor */
+ private IClojureEditor editor;
+
+ /** Document (mutable) */
+ private IDocument document;
+
+ /** Progress monitor (mutable) */
+ private IProgressMonitor progressMonitor;
+
+ public FoldingReconcileStrategy(IClojureEditor editor) {
+ this.editor = editor;
+ }
+
+ @Override
+ public void setProgressMonitor(IProgressMonitor monitor) {
+ progressMonitor = monitor;
+ }
+
+ @Override
+ public void initialReconcile() {
+ CCWPlugin.getTracer().trace(TraceOptions.EDITOR_TEXT, "Starting initialReconcile...");
+ foldingSupport._("compute-folding!", editor);
+ }
+
+ @Override
+ public void setDocument(IDocument document) {
+ this.document = document;
+ }
+
+ @Override
+ public void reconcile(DirtyRegion dirtyRegion, IRegion subRegion) {
+ throw new RuntimeException("Incremental reconciler not implemented yet");
+ }
+
+ @Override
+ public void reconcile(IRegion partition) {
+ CCWPlugin.getTracer().trace(TraceOptions.EDITOR_TEXT, "Reconcile partition: " + partition);
+ foldingSupport._("compute-folding!", editor);
+ }
+}
diff --git a/ccw.core/src/java/ccw/preferences/FoldingPreferencePage.java b/ccw.core/src/java/ccw/preferences/FoldingPreferencePage.java
new file mode 100644
index 00000000..69eda681
--- /dev/null
+++ b/ccw.core/src/java/ccw/preferences/FoldingPreferencePage.java
@@ -0,0 +1,253 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Laurent Petit.
+ * 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:
+ * Andrea RICHIARDI - initial implementation
+ *******************************************************************************/
+package ccw.preferences;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import org.eclipse.core.databinding.DataBindingContext;
+import org.eclipse.core.databinding.observable.map.IObservableMap;
+import org.eclipse.core.databinding.observable.value.ComputedValue;
+import org.eclipse.core.databinding.observable.value.IObservableValue;
+import org.eclipse.core.databinding.property.Properties;
+import org.eclipse.core.runtime.Platform;
+import org.eclipse.core.runtime.preferences.InstanceScope;
+import org.eclipse.e4.core.contexts.ContextInjectionFactory;
+import org.eclipse.jface.databinding.preference.PreferencePageSupport;
+import org.eclipse.jface.databinding.swt.WidgetProperties;
+import org.eclipse.jface.databinding.viewers.ObservableListContentProvider;
+import org.eclipse.jface.databinding.viewers.ObservableMapLabelProvider;
+import org.eclipse.jface.databinding.viewers.ViewersObservables;
+import org.eclipse.jface.dialogs.Dialog;
+import org.eclipse.jface.layout.TableColumnLayout;
+import org.eclipse.jface.preference.PreferencePage;
+import org.eclipse.jface.viewers.CheckboxTableViewer;
+import org.eclipse.jface.viewers.ColumnViewerToolTipSupport;
+import org.eclipse.jface.viewers.ColumnWeightData;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.window.ToolTip;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.graphics.Point;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.layout.GridData;
+import org.eclipse.swt.layout.GridLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Group;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.TableColumn;
+import org.eclipse.ui.IWorkbench;
+import org.eclipse.ui.IWorkbenchPreferencePage;
+import org.osgi.service.prefs.BackingStoreException;
+
+import ccw.CCWPlugin;
+import ccw.core.StaticStrings;
+import ccw.editors.clojure.folding.FoldingDescriptor;
+import ccw.editors.clojure.folding.FoldingModel;
+
+/**
+ * Configures Clojure's folding preferences.
+ *
+ */
+public class FoldingPreferencePage extends PreferencePage implements IWorkbenchPreferencePage {
+
+ // Injected
+ private @Inject @Named(StaticStrings.CCW_CONTEXT_VALUE_FOLDINGMODEL) FoldingModel fModel; // Model
+
+ // Manually Wired
+ FoldingViewModel fViewModel; // ViewModel
+
+ // Widgets
+ private CheckboxTableViewer fFoldingTableViewer;
+
+ private Group fGrpSummary;
+ private Label fSummaryFoldTargetLabel;
+ private Label fSummaryDescriptionLabel;
+
+ @Override
+ public void init(IWorkbench workbench) {
+ // AR - Note: init() will always be called once, the constructor can be called multiple times
+ ContextInjectionFactory.inject(this, CCWPlugin.getEclipseContext());
+ fViewModel = new FoldingViewModel(fModel);
+
+ setPreferenceStore(CCWPlugin.getDefault().getPreferenceStore());
+ }
+
+ /**
+ * @wbp.parser.constructor
+ */
+ public FoldingPreferencePage() {
+ }
+
+ /**
+ * Creates page for folding preferences.
+ *
+ * @param parent
+ * the parent composite
+ * @return the control for the preference page
+ */
+ public Control createContents(Composite parent) {
+
+ ScrolledPageContent scrolled = new ScrolledPageContent(parent, SWT.H_SCROLL | SWT.V_SCROLL);
+ scrolled.setExpandHorizontal(true);
+ scrolled.setExpandVertical(true);
+
+ Composite cointainerComposite = new Composite(scrolled, SWT.NONE);
+ FillLayout fillLayout = new FillLayout(SWT.VERTICAL);
+ cointainerComposite.setLayout(fillLayout);
+
+ // Table
+ Composite tableComposite = new Composite(cointainerComposite, SWT.NONE);
+
+ fFoldingTableViewer = CheckboxTableViewer.newCheckList(tableComposite, SWT.H_SCROLL | SWT.V_SCROLL | SWT.SINGLE
+ | SWT.BORDER | SWT.FULL_SELECTION | SWT.CHECK);
+ ColumnViewerToolTipSupport.enableFor(fFoldingTableViewer, ToolTip.NO_RECREATE);
+
+ // Setup
+ fFoldingTableViewer.setUseHashlookup(true);
+ fFoldingTableViewer.getTable().setHeaderVisible(true);
+ fFoldingTableViewer.getTable().setLinesVisible(true);
+
+ TableColumnLayout layout = new TableColumnLayout();
+ tableComposite.setLayout(layout);
+
+ TableViewerColumn viewerColumn = new TableViewerColumn(fFoldingTableViewer, SWT.NONE);
+ TableColumn column = viewerColumn.getColumn();
+ column.setText(Messages.FoldingPreferencePage_labelColumnTitle);
+ column.setResizable(true);
+ column.setMoveable(true);
+ column.setResizable(true);
+ layout.setColumnData(column, new ColumnWeightData(35, true));
+
+ viewerColumn = new TableViewerColumn(fFoldingTableViewer, SWT.NONE);
+ column = viewerColumn.getColumn();
+ column.setText(Messages.FoldingPreferencePage_descriptionColumnTitle);
+ column.setResizable(true);
+ column.setMoveable(true);
+ layout.setColumnData(column, new ColumnWeightData(40, true));
+
+ // Summary
+ Composite summaryComposite = new Composite(cointainerComposite, SWT.NONE);
+ summaryComposite.setLayout(new FillLayout(SWT.HORIZONTAL));
+
+ fGrpSummary = new Group(summaryComposite, SWT.NONE);
+ GridLayout gridLayout = new GridLayout(2, false);
+ gridLayout.marginLeft = 4;
+ gridLayout.marginRight = 4;
+ gridLayout.horizontalSpacing = 8;
+ gridLayout.verticalSpacing = 8;
+ gridLayout.marginTop = 4;
+ gridLayout.marginHeight = 0;
+ gridLayout.marginWidth = 0;
+ fGrpSummary.setLayout(gridLayout);
+
+ // Summary Label
+ fSummaryFoldTargetLabel = new Label(fGrpSummary, SWT.HORIZONTAL);
+ GridData fieldLayoutData = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING);
+ fieldLayoutData.widthHint = 120;
+ fSummaryFoldTargetLabel.setLayoutData(fieldLayoutData);
+
+ // Summary Description
+ fSummaryDescriptionLabel = new Label(fGrpSummary, SWT.BORDER | SWT.WRAP);
+ fieldLayoutData = new GridData(SWT.FILL, SWT.BEGINNING, true, false);
+ fieldLayoutData.heightHint = 100;
+ fieldLayoutData.horizontalSpan = 2;
+ fSummaryDescriptionLabel.setLayoutData(fieldLayoutData);
+
+ summaryComposite.pack();
+
+ // End Summary
+
+ scrolled.setContent(cointainerComposite);
+ final Point size = cointainerComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT);
+ scrolled.setMinSize(size.x, size.y);
+
+ Dialog.applyDialogFont(scrolled);
+
+ initDataBindings();
+
+ return scrolled;
+ }
+
+ protected DataBindingContext initDataBindings() {
+
+ final DataBindingContext context = new DataBindingContext();
+
+ // Page Support
+ PreferencePageSupport.create(this, context);
+
+ // ///////////////////////////
+ // Label/Content providers \\
+ // ///////////////////////////
+
+ ObservableListContentProvider contentProvider = new ObservableListContentProvider();
+
+ IObservableMap[] columnsObservables = Properties.observeEach(contentProvider.getKnownElements(),
+ FoldingViewModel.displayDomain);
+
+ ObservableMapLabelProvider labelProvider = new ObservableMapLabelProvider(columnsObservables);
+
+ fFoldingTableViewer.setLabelProvider(labelProvider);
+ fFoldingTableViewer.setContentProvider(contentProvider);
+ fFoldingTableViewer.setInput(fViewModel.observableList);
+
+ /////////////////////
+ // Selection logic \\
+ /////////////////////
+
+ final IObservableValue selected = ViewersObservables.observeSingleSelection(fFoldingTableViewer);
+
+ final IObservableValue isSelected = new ComputedValue(Boolean.TYPE) {
+ @Override
+ protected Object calculate() {
+ return Boolean.valueOf(selected.getValue() != null);
+ }
+ };
+
+ context.bindValue(WidgetProperties.enabled().observe(fGrpSummary), isSelected);
+
+ if (!fViewModel.observableList.isEmpty()) {
+ selected.setValue(fViewModel.observableList.get(0));
+ }
+
+ /////////////////
+ // Check logic \\
+ /////////////////
+
+ context.bindSet(ViewersObservables.observeCheckedElements(fFoldingTableViewer, FoldingDescriptor.class),
+ fViewModel.checkedSet);
+
+ /////////////////
+ // UI Bindings \\
+ /////////////////
+
+ context.bindValue(WidgetProperties.text().observe(fSummaryFoldTargetLabel),
+ FoldingViewModel.labelProperty.observeDetail(selected));
+
+ context.bindValue(WidgetProperties.text().observe(fSummaryDescriptionLabel),
+ FoldingViewModel.descriptionProperty.observeDetail(selected));
+
+ return context;
+ }
+
+ public boolean performOk() {
+ fModel.persistDescriptors(fViewModel.observableList);
+
+ boolean result = true;
+ try {
+ Platform.getPreferencesService().getRootNode().node(InstanceScope.SCOPE).node(CCWPlugin.PLUGIN_ID).flush();
+ } catch (BackingStoreException e) {
+ CCWPlugin.logError("Saving Preferences failed", e);
+ result = false;
+ }
+ return result;
+ }
+}
diff --git a/ccw.core/src/java/ccw/preferences/FoldingViewModel.java b/ccw.core/src/java/ccw/preferences/FoldingViewModel.java
new file mode 100644
index 00000000..f68cd0ef
--- /dev/null
+++ b/ccw.core/src/java/ccw/preferences/FoldingViewModel.java
@@ -0,0 +1,95 @@
+/*******************************************************************************
+ * Copyright (c) 2015 Laurent Petit.
+ * 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:
+ * Andrea RICHIARDI - initial implementation
+ *******************************************************************************/
+package ccw.preferences;
+
+import java.util.List;
+import java.util.Set;
+
+import javax.inject.Singleton;
+
+import org.eclipse.core.databinding.beans.BeanProperties;
+import org.eclipse.core.databinding.observable.list.IListChangeListener;
+import org.eclipse.core.databinding.observable.list.IObservableList;
+import org.eclipse.core.databinding.observable.list.ListChangeEvent;
+import org.eclipse.core.databinding.observable.set.IObservableSet;
+import org.eclipse.core.databinding.observable.set.ISetChangeListener;
+import org.eclipse.core.databinding.observable.set.SetChangeEvent;
+import org.eclipse.core.databinding.observable.set.SetDiff;
+import org.eclipse.core.databinding.observable.set.WritableSet;
+import org.eclipse.core.databinding.property.value.IValueProperty;
+
+import ccw.editors.clojure.folding.FoldingDescriptor;
+import ccw.editors.clojure.folding.FoldingModel;
+
+/**
+ * The ViewModel of the Hover Preference page.
+ *
+ * @author Andrea Richiardi
+ *
+ */
+@Singleton
+public final class FoldingViewModel {
+
+ // Observables
+ public final IObservableSet checkedSet;
+ public final IObservableList observableList;
+
+ // Properties
+ public final static IValueProperty labelProperty = BeanProperties.value(FoldingDescriptor.class, "label");
+ public final static IValueProperty enabledProperty = BeanProperties.value(FoldingDescriptor.class, "enabled");
+ public final static IValueProperty descriptionProperty = BeanProperties.value(FoldingDescriptor.class, "description");
+ public final static IValueProperty[] displayDomain = new IValueProperty[] { labelProperty, descriptionProperty };
+
+ // Validators
+ // None
+
+ @SuppressWarnings("unchecked")
+ FoldingViewModel(FoldingModel model) {
+
+ observableList = model.getObservableDescriptors();
+ observableList.addListChangeListener(new IListChangeListener() {
+ @Override
+ public void handleListChange(ListChangeEvent event) {
+ updateCheckedSet((List) event.getObservableList());
+ }
+ });
+
+ checkedSet = new WritableSet();
+ updateCheckedSet((List) observableList);
+
+ checkedSet.addSetChangeListener(new ISetChangeListener() {
+ private SetDiff fDiff;
+
+ @Override
+ public void handleSetChange(SetChangeEvent event) {
+ fDiff = event.diff;
+
+ for (FoldingDescriptor hd : (Set) fDiff.getAdditions()) {
+ hd.setEnabled(true);
+ }
+
+ for (FoldingDescriptor hd : (Set) fDiff.getRemovals()) {
+ hd.setEnabled(false);
+ }
+ }
+ });
+ }
+
+ private void updateCheckedSet(List descriptors) {
+ checkedSet.clear();
+
+ for (FoldingDescriptor hd : descriptors) {
+ if (hd.isEnabled() == Boolean.TRUE) {
+ checkedSet.add(hd);
+ }
+ }
+ }
+}
diff --git a/ccw.core/src/java/ccw/preferences/Messages.java b/ccw.core/src/java/ccw/preferences/Messages.java
index eec645e7..d166230b 100644
--- a/ccw.core/src/java/ccw/preferences/Messages.java
+++ b/ccw.core/src/java/ccw/preferences/Messages.java
@@ -158,6 +158,14 @@ public class Messages extends NLS {
public static String HoverPreferencePage_labelColumnTitle;
public static String HoverPreferencePage_modifierColumnTitle;
+ public static String FoldingPreferencePage_description;
+ public static String FoldingPreferencePage_descriptionColumnTitle;
+ public static String FoldingPreferencePage_labelColumnTitle;
+ public static String FoldingPreferencePage_fold_parens_label;
+ public static String FoldingPreferencePage_fold_parens_description;
+ public static String FoldingPreferencePage_fold_double_apex_label;
+ public static String FoldingPreferencePage_fold_double_apex_description;
+
static {
// initialize resource bundle
NLS.initializeMessages(BUNDLE_NAME, Messages.class);
diff --git a/ccw.core/src/java/ccw/preferences/PreferenceConstants.java b/ccw.core/src/java/ccw/preferences/PreferenceConstants.java
index 2e85a2aa..93630f48 100644
--- a/ccw.core/src/java/ccw/preferences/PreferenceConstants.java
+++ b/ccw.core/src/java/ccw/preferences/PreferenceConstants.java
@@ -201,6 +201,12 @@ public class PreferenceConstants {
*/
public static final String EDITOR_SHOW_TEXT_HOVER_AFFORDANCE= CCW_PREFERENCE_PREFIX + ".show_hover_affordance"; //$NON-NLS-1$
+ /**
+ * A named preference that defines the folding descriptors (see editors.clojure.folding-support)
+ */
+ public static final String EDITOR_FOLDING_DESCRIPTORS = CCW_PREFERENCE_PREFIX + ".folding.descriptors"; //$NON-NLS-1$
+ public static final String EDITOR_FOLDING_PROJECTION_ENABLED = CCW_PREFERENCE_PREFIX + ".folding.projection-enabled";
+
public static class ColorizableToken {
public final @NonNull RGB rgb;
public final @Nullable Boolean isBold;
diff --git a/ccw.core/src/java/ccw/preferences/PreferenceInitializer.java b/ccw.core/src/java/ccw/preferences/PreferenceInitializer.java
index 55a5effc..4c372057 100644
--- a/ccw.core/src/java/ccw/preferences/PreferenceInitializer.java
+++ b/ccw.core/src/java/ccw/preferences/PreferenceInitializer.java
@@ -72,6 +72,18 @@ public class PreferenceInitializer extends AbstractPreferenceInitializer {
};
public static final String DEFAULT_EDITOR_TEXT_HOVER_DESCRIPTORS = "()";
+ public static final String DEFAULT_FOLDING_DESCRIPTORS =
+ "({:id :fold-parens"
+ + " :enabled true"
+ + " :loc-tags #{:list}"
+ + " :label \"" + Messages.FoldingPreferencePage_fold_parens_label + "\""
+ + " :description \"" + Messages.FoldingPreferencePage_fold_parens_description + "\"}"
+ + "{:id :fold-double-apices "
+ + " :enabled true"
+ + " :loc-tags #{:string}"
+ + " :label \"" + Messages.FoldingPreferencePage_fold_double_apex_label + "\""
+ + " :description \"" + Messages.FoldingPreferencePage_fold_double_apex_description + "\"})";
+ public static final Boolean DEFAULT_PROJECTION_ENABLED = Boolean.TRUE;
@Override
public void initializeDefaultPreferences() {
@@ -122,5 +134,9 @@ public void initializeDefaultPreferences() {
// Hover pref
store.setDefault(PreferenceConstants.EDITOR_TEXT_HOVER_DESCRIPTORS, DEFAULT_EDITOR_TEXT_HOVER_DESCRIPTORS);
+
+ // Folding pref
+ store.setDefault(PreferenceConstants.EDITOR_FOLDING_DESCRIPTORS, DEFAULT_FOLDING_DESCRIPTORS);
+ store.setDefault(PreferenceConstants.EDITOR_FOLDING_PROJECTION_ENABLED, DEFAULT_PROJECTION_ENABLED);
}
}
diff --git a/ccw.core/src/java/ccw/preferences/SyntaxColoringPreferencePage.java b/ccw.core/src/java/ccw/preferences/SyntaxColoringPreferencePage.java
index dd96ca4b..dc4f0b45 100644
--- a/ccw.core/src/java/ccw/preferences/SyntaxColoringPreferencePage.java
+++ b/ccw.core/src/java/ccw/preferences/SyntaxColoringPreferencePage.java
@@ -71,7 +71,7 @@
import ccw.editors.clojure.ClojureSourceViewer;
import ccw.editors.clojure.SimpleSourceViewerConfiguration;
import ccw.editors.clojure.scanners.ClojurePartitionScanner;
-import ccw.editors.clojure.scanners.ClojurePartitioner;
+import ccw.editors.clojure.scanners.TracingPartitioner;
import ccw.repl.REPLView;
import ccw.repl.SafeConnection;
@@ -796,7 +796,7 @@ public void setStatusLineErrorMessage(String you_need_a_running_repl) {
fPreviewViewer.getTextWidget().setFont(font);
IDocument document= new Document(PREVIEW_SOURCE);
- IDocumentPartitioner partitioner = new ClojurePartitioner(new ClojurePartitionScanner(),
+ IDocumentPartitioner partitioner = new TracingPartitioner(new ClojurePartitionScanner(),
ClojurePartitionScanner.CLOJURE_CONTENT_TYPES);
Map m = new HashMap();
diff --git a/ccw.core/src/java/ccw/preferences/messages.properties b/ccw.core/src/java/ccw/preferences/messages.properties
index f169e2c5..57662f53 100644
--- a/ccw.core/src/java/ccw/preferences/messages.properties
+++ b/ccw.core/src/java/ccw/preferences/messages.properties
@@ -85,3 +85,11 @@ REPLViewPreferencePage_pprintRightMargin=Right margin column to use for pprint:
REPLHistoryPreferencePage_max_size=Maximum REPL history size:
REPLHistoryPreferencePage_persist_schedule=REPL history persistence schedule [ms]:
+
+FoldingPreferencePage_description=Description\:
+FoldingPreferencePage_descriptionColumnTitle=Description
+FoldingPreferencePage_labelColumnTitle=Fold target
+FoldingPreferencePage_fold_parens_label=Parens
+FoldingPreferencePage_fold_parens_description=Folds multi-line top-level lists
+FoldingPreferencePage_fold_double_apex_label=Double-apices
+FoldingPreferencePage_fold_double_apex_description=Folds multi-line strings
\ No newline at end of file
diff --git a/ccw.core/src/java/ccw/util/TestUtil.java b/ccw.core/src/java/ccw/util/TestUtil.java
index aaadb745..b3914c5a 100644
--- a/ccw.core/src/java/ccw/util/TestUtil.java
+++ b/ccw.core/src/java/ccw/util/TestUtil.java
@@ -1,7 +1,17 @@
package ccw.util;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import org.eclipse.jdt.annotation.NonNull;
+import org.eclipse.jface.text.Position;
+import org.eclipse.jface.text.source.Annotation;
+import org.eclipse.jface.text.source.projection.ProjectionAnnotationModel;
import org.eclipse.swt.widgets.Widget;
+import ccw.editors.clojure.IClojureEditor;
+
public final class TestUtil {
/**
@@ -12,4 +22,23 @@ public static W setTestId(W w, String id) {
w.setData("org.eclipse.swtbot.widget.key", id);
return w;
}
+
+ /**
+ * Given a ClojureEditor, return a map Annotation keys to
+ * Position values, got from its ProjectionAnnotationModel.
+ * @param editor
+ * @return A Map, potentially empty.
+ */
+ public static @NonNull Map getProjectionMap(IClojureEditor editor) {
+ Map m = new HashMap();
+
+ ProjectionAnnotationModel model = editor.getProjectionAnnotationModel();
+ Iterator annotations = model.getAnnotationIterator();
+ while (annotations.hasNext()) {
+ Annotation annotation = (Annotation) annotations.next();
+ Position position = model.getPosition(annotation);
+ m.put(annotation, position);
+ }
+ return m;
+ }
}