diff --git a/CHANGELOG.md b/CHANGELOG.md index 74832a8e..f961a697 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # CheckStyle-IDEA Changelog +* **5.99.0** New: Added option to group results by package (#3). * **5.98.0** New: Added Checkstyle 10.20.1. * **5.97.0** Fixed: Refactored code to fix exception around API dependencies at initialisation (#655). * **5.97.0** New: Added Checkstyle 10.19.0. diff --git a/build.gradle.kts b/build.gradle.kts index b653d552..992243e5 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,7 +19,7 @@ plugins { id("org.infernus.idea.checkstyle.build") } -version = "5.98.0" +version = "5.99.0" intellijPlatform { pluginConfiguration { diff --git a/buildSrc/src/main/java/org/infernus/idea/checkstyle/build/GradlePluginMain.java b/buildSrc/src/main/java/org/infernus/idea/checkstyle/build/GradlePluginMain.java index 3d5f5022..f913cf35 100644 --- a/buildSrc/src/main/java/org/infernus/idea/checkstyle/build/GradlePluginMain.java +++ b/buildSrc/src/main/java/org/infernus/idea/checkstyle/build/GradlePluginMain.java @@ -21,8 +21,6 @@ import org.gradle.language.base.plugins.LifecycleBasePlugin; import org.jetbrains.annotations.NotNull; -import static org.codehaus.groovy.runtime.DefaultGroovyMethods.println; - /** * The main plugin class. The action starts here. */ diff --git a/src/main/java/org/infernus/idea/checkstyle/actions/GroupByFile.java b/src/main/java/org/infernus/idea/checkstyle/actions/GroupByFile.java new file mode 100644 index 00000000..51735b3c --- /dev/null +++ b/src/main/java/org/infernus/idea/checkstyle/actions/GroupByFile.java @@ -0,0 +1,10 @@ +package org.infernus.idea.checkstyle.actions; + +import org.infernus.idea.checkstyle.toolwindow.ResultGrouping; + +public class GroupByFile extends GroupingAction { + + public GroupByFile() { + super(ResultGrouping.BY_FILE); + } +} diff --git a/src/main/java/org/infernus/idea/checkstyle/actions/GroupByPackage.java b/src/main/java/org/infernus/idea/checkstyle/actions/GroupByPackage.java new file mode 100644 index 00000000..c4ed37d0 --- /dev/null +++ b/src/main/java/org/infernus/idea/checkstyle/actions/GroupByPackage.java @@ -0,0 +1,10 @@ +package org.infernus.idea.checkstyle.actions; + +import org.infernus.idea.checkstyle.toolwindow.ResultGrouping; + +public class GroupByPackage extends GroupingAction { + + public GroupByPackage() { + super(ResultGrouping.BY_PACKAGE); + } +} diff --git a/src/main/java/org/infernus/idea/checkstyle/actions/GroupingAction.java b/src/main/java/org/infernus/idea/checkstyle/actions/GroupingAction.java new file mode 100644 index 00000000..6f63290a --- /dev/null +++ b/src/main/java/org/infernus/idea/checkstyle/actions/GroupingAction.java @@ -0,0 +1,49 @@ +package org.infernus.idea.checkstyle.actions; + +import com.intellij.openapi.actionSystem.ActionUpdateThread; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.project.DumbAwareToggleAction; +import com.intellij.openapi.project.Project; +import org.infernus.idea.checkstyle.toolwindow.ResultGrouping; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +import static org.infernus.idea.checkstyle.actions.ToolWindowAccess.*; + +abstract class GroupingAction extends DumbAwareToggleAction { + + private final ResultGrouping grouping; + + GroupingAction(@NotNull final ResultGrouping grouping) { + this.grouping = grouping; + } + + @Override + public boolean isSelected(final @NotNull AnActionEvent event) { + final Project project = getEventProject(event); + if (project == null) { + return false; + } + + Boolean selected = getFromToolWindowPanel(toolWindow(project), panel -> panel.groupedBy() == grouping); + return Objects.requireNonNullElse(selected, false); + } + + @Override + public void setSelected(final @NotNull AnActionEvent event, final boolean selected) { + final Project project = getEventProject(event); + if (project == null) { + return; + } + + actOnToolWindowPanel(toolWindow(project), panel -> { + panel.groupBy(grouping); + }); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.EDT; + } +} diff --git a/src/main/java/org/infernus/idea/checkstyle/toolwindow/CheckStyleToolWindowPanel.java b/src/main/java/org/infernus/idea/checkstyle/toolwindow/CheckStyleToolWindowPanel.java index 2b5a9e4f..dd415084 100644 --- a/src/main/java/org/infernus/idea/checkstyle/toolwindow/CheckStyleToolWindowPanel.java +++ b/src/main/java/org/infernus/idea/checkstyle/toolwindow/CheckStyleToolWindowPanel.java @@ -40,10 +40,8 @@ import javax.swing.tree.TreePath; import java.awt.*; import java.awt.event.*; -import java.util.ArrayList; -import java.util.HashMap; +import java.util.*; import java.util.List; -import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -297,7 +295,7 @@ private void clearProgress() { */ private void scrollToError(final TreePath treePath) { final DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) treePath.getLastPathComponent(); - if (treeNode == null || !(treeNode.getUserObject() instanceof ProblemResultTreeNode nodeInfo)) { + if (treeNode == null || !(treeNode.getUserObject() instanceof ProblemResultTreeInfo nodeInfo)) { return; } @@ -326,11 +324,11 @@ private void scrollToError(final TreePath treePath) { }, ModalityState.NON_MODAL); } - private int lineFor(final ProblemResultTreeNode nodeInfo) { + private int lineFor(final ProblemResultTreeInfo nodeInfo) { return nodeInfo.getProblem().line(); } - private int columnFor(final ProblemResultTreeNode nodeInfo) { + private int columnFor(final ProblemResultTreeInfo nodeInfo) { return nodeInfo.getProblem().column(); } @@ -521,8 +519,8 @@ public void displayErrorResult(final Throwable error) { clearProgress(); } - private SeverityLevel[] getDisplayedSeverities() { - final List severityLevels = new ArrayList<>(); + private Set getDisplayedSeverities() { + final var severityLevels = new HashSet(); if (displayingErrors) { severityLevels.add(SeverityLevel.Error); @@ -536,7 +534,7 @@ private SeverityLevel[] getDisplayedSeverities() { severityLevels.add(SeverityLevel.Info); } - return severityLevels.toArray(new SeverityLevel[0]); + return severityLevels; } /** @@ -586,6 +584,14 @@ public void setDisplayingInfo(final boolean displayingInfo) { this.displayingInfo = displayingInfo; } + public void groupBy(final ResultGrouping grouping) { + treeModel.groupBy(grouping); + } + + public ResultGrouping groupedBy() { + return treeModel.groupedBy(); + } + private PluginConfigurationManager configurationManager() { return project.getService(PluginConfigurationManager.class); } diff --git a/src/main/java/org/infernus/idea/checkstyle/toolwindow/FileResultTreeNode.java b/src/main/java/org/infernus/idea/checkstyle/toolwindow/FileResultTreeInfo.java similarity index 91% rename from src/main/java/org/infernus/idea/checkstyle/toolwindow/FileResultTreeNode.java rename to src/main/java/org/infernus/idea/checkstyle/toolwindow/FileResultTreeInfo.java index 88a00b75..12fc0e53 100644 --- a/src/main/java/org/infernus/idea/checkstyle/toolwindow/FileResultTreeNode.java +++ b/src/main/java/org/infernus/idea/checkstyle/toolwindow/FileResultTreeInfo.java @@ -3,7 +3,7 @@ import com.intellij.icons.AllIcons; import org.infernus.idea.checkstyle.CheckStyleBundle; -public class FileResultTreeNode extends ResultTreeNode { +public class FileResultTreeInfo extends ResultTreeNode { private final String fileName; private final int totalProblems; @@ -15,7 +15,7 @@ public class FileResultTreeNode extends ResultTreeNode { * @param fileName the name of the file. * @param problemCount the number of problems in the file. */ - public FileResultTreeNode(final String fileName, final int problemCount) { + public FileResultTreeInfo(final String fileName, final int problemCount) { super(CheckStyleBundle.message("plugin.results.scan-file-result", fileName, problemCount)); if (fileName == null) { diff --git a/src/main/java/org/infernus/idea/checkstyle/toolwindow/PackageTreeInfo.java b/src/main/java/org/infernus/idea/checkstyle/toolwindow/PackageTreeInfo.java new file mode 100644 index 00000000..fa411460 --- /dev/null +++ b/src/main/java/org/infernus/idea/checkstyle/toolwindow/PackageTreeInfo.java @@ -0,0 +1,47 @@ +package org.infernus.idea.checkstyle.toolwindow; + +import com.intellij.icons.AllIcons; +import org.infernus.idea.checkstyle.CheckStyleBundle; + +public class PackageTreeInfo extends ResultTreeNode { + + private final String packageName; + private final int totalProblems; + private int visibleProblems; + + /** + * Construct a package node. + * + * @param packageName the name of the package. + * @param problemCount the number of problems in the file. + */ + public PackageTreeInfo(final String packageName, final int problemCount) { + super(CheckStyleBundle.message("plugin.results.scan-file-result", packageName, problemCount)); + + if (packageName == null) { + throw new IllegalArgumentException("Package name may not be null"); + } + + this.packageName = packageName; + this.totalProblems = problemCount; + this.visibleProblems = problemCount; + + updateDisplayText(); + + setIcon(AllIcons.Nodes.Package); + } + + private void updateDisplayText() { + if (totalProblems == visibleProblems) { + setText(CheckStyleBundle.message("plugin.results.scan-package-result", packageName, totalProblems)); + } else { + setText(CheckStyleBundle.message("plugin.results.scan-package-result.filtered", packageName, visibleProblems, totalProblems - visibleProblems)); + } + } + + void setVisibleProblems(final int visibleProblems) { + this.visibleProblems = visibleProblems; + + updateDisplayText(); + } +} diff --git a/src/main/java/org/infernus/idea/checkstyle/toolwindow/ProblemResultTreeNode.java b/src/main/java/org/infernus/idea/checkstyle/toolwindow/ProblemResultTreeInfo.java similarity index 94% rename from src/main/java/org/infernus/idea/checkstyle/toolwindow/ProblemResultTreeNode.java rename to src/main/java/org/infernus/idea/checkstyle/toolwindow/ProblemResultTreeInfo.java index c6a34b64..6439d8b5 100644 --- a/src/main/java/org/infernus/idea/checkstyle/toolwindow/ProblemResultTreeNode.java +++ b/src/main/java/org/infernus/idea/checkstyle/toolwindow/ProblemResultTreeInfo.java @@ -7,7 +7,7 @@ import org.infernus.idea.checkstyle.csapi.SeverityLevel; import org.jetbrains.annotations.NotNull; -public class ProblemResultTreeNode extends ResultTreeNode { +public class ProblemResultTreeInfo extends ResultTreeNode { private final PsiFile file; private final Problem problem; @@ -19,7 +19,7 @@ public class ProblemResultTreeNode extends ResultTreeNode { * @param file the file the problem exists in. * @param problem the problem. */ - public ProblemResultTreeNode(@NotNull final PsiFile file, + public ProblemResultTreeInfo(@NotNull final PsiFile file, @NotNull final Problem problem) { super(CheckStyleBundle.message("plugin.results.file-result", file.getName(), diff --git a/src/main/java/org/infernus/idea/checkstyle/toolwindow/ResultGrouping.java b/src/main/java/org/infernus/idea/checkstyle/toolwindow/ResultGrouping.java new file mode 100644 index 00000000..1f9912f6 --- /dev/null +++ b/src/main/java/org/infernus/idea/checkstyle/toolwindow/ResultGrouping.java @@ -0,0 +1,8 @@ +package org.infernus.idea.checkstyle.toolwindow; + +public enum ResultGrouping { + + BY_FILE, + BY_PACKAGE + +} diff --git a/src/main/java/org/infernus/idea/checkstyle/toolwindow/ResultTreeModel.java b/src/main/java/org/infernus/idea/checkstyle/toolwindow/ResultTreeModel.java index b1fc127b..f245c5a5 100644 --- a/src/main/java/org/infernus/idea/checkstyle/toolwindow/ResultTreeModel.java +++ b/src/main/java/org/infernus/idea/checkstyle/toolwindow/ResultTreeModel.java @@ -8,20 +8,28 @@ import com.intellij.psi.PsiFile; import com.intellij.psi.PsiFileSystemItem; +import com.intellij.psi.PsiJavaFile; import org.infernus.idea.checkstyle.CheckStyleBundle; import org.infernus.idea.checkstyle.checker.Problem; import org.infernus.idea.checkstyle.csapi.SeverityLevel; +import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import static java.util.Collections.emptyList; import static java.util.Comparator.*; public class ResultTreeModel extends DefaultTreeModel { @Serial private static final long serialVersionUID = 2161855162879365203L; + public static final Set DEFAULT_SEVERITIES = Set.of(SeverityLevel.Error, SeverityLevel.Warning, SeverityLevel.Info); private final ToggleableTreeNode visibleRootNode; + private Set displayedSeverities = DEFAULT_SEVERITIES; + private ResultGrouping grouping = ResultGrouping.BY_FILE; + private Map> lastResults; + public ResultTreeModel() { super(new DefaultMutableTreeNode()); @@ -67,66 +75,71 @@ public void setRootMessage(@Nullable final String messageKey, @Nullable final Object... messageArgs) { if (messageKey == null) { setRootText(null); - } else { setRootText(CheckStyleBundle.message(messageKey, messageArgs)); } } + private void rebuildTree() { + visibleRootNode.removeAllChildren(); + + switch (grouping) { + case BY_FILE -> groupResultsByFile(); + case BY_PACKAGE -> groupResultsByPackage(); + } + + filterDisplayedTree(); + nodeStructureChanged(visibleRootNode); + } + /** * Display only the passed severity levels. * - * @param levels the levels. Null is treated as 'none'. + * @param severityLevels the levels. An empty set is treated as 'none'. */ - public void filter(final SeverityLevel... levels) { - filter(true, levels); + public void filter(@NotNull final Set severityLevels) { + this.displayedSeverities = severityLevels; + + filterDisplayedTree(); + nodeStructureChanged(visibleRootNode); } - private void filter(final boolean sendEvents, final SeverityLevel... levels) { - for (final ToggleableTreeNode fileNode : visibleRootNode.getAllChildren()) { - for (final ToggleableTreeNode problemNode : fileNode.getAllChildren()) { - final ProblemResultTreeNode result = (ProblemResultTreeNode) problemNode.getUserObject(); + public void groupBy(@NotNull final ResultGrouping resultGrouping) { + this.grouping = resultGrouping; - final boolean resultShouldBeVisible = contains(levels, result.getSeverity()); - if (problemNode.isVisible() != resultShouldBeVisible) { - problemNode.setVisible(resultShouldBeVisible); - } - } + rebuildTree(); + } - ((FileResultTreeNode) fileNode.getUserObject()).setVisibleProblems(fileNode.getChildCount()); - final boolean fileNodeShouldBeVisible = fileNode.getChildCount() > 0; - if (fileNode.isVisible() != fileNodeShouldBeVisible) { - fileNode.setVisible(fileNodeShouldBeVisible); - } - } + public ResultGrouping groupedBy() { + return grouping; + } - if (sendEvents) { - nodeStructureChanged(visibleRootNode); - } + private void filterDisplayedTree() { + filterNodeAndChildren(visibleRootNode); } - /* - * This is a port from commons-lang 2.4, in order to get around the absence of commons-lang in - * some packages of IDEA 7.x. - */ - private boolean contains(final Object[] array, final Object objectToFind) { - if (array == null) { - return false; + private void filterNodeAndChildren(final ToggleableTreeNode node) { + boolean nodeShouldBeVisible = true; + + for (final var childNode : node.getAllChildren()) { + filterNodeAndChildren(childNode); } - if (objectToFind == null) { - for (final Object anArray : array) { - if (anArray == null) { - return true; - } - } - } else { - for (final Object anArray : array) { - if (objectToFind.equals(anArray)) { - return true; - } - } + + if (node.getUserObject() instanceof FileResultTreeInfo fileResultTreeInfo) { + fileResultTreeInfo.setVisibleProblems(node.getChildCount()); + nodeShouldBeVisible = node.getChildCount() > 0; + + } else if (node.getUserObject() instanceof PackageTreeInfo packageTreeInfo) { + packageTreeInfo.setVisibleProblems(node.getChildCount()); + nodeShouldBeVisible = node.getChildCount() > 0; + + } else if (node.getUserObject() instanceof ProblemResultTreeInfo problemResultTreeInfo) { + nodeShouldBeVisible = displayedSeverities.contains(problemResultTreeInfo.getSeverity()); + } + + if (node.isVisible() != nodeShouldBeVisible) { + node.setVisible(nodeShouldBeVisible); } - return false; } /** @@ -134,8 +147,8 @@ private boolean contains(final Object[] array, final Object objectToFind) { * * @param results the model. */ - public void setModel(final Map> results) { - setModel(results, SeverityLevel.Error, SeverityLevel.Warning, SeverityLevel.Info); + public void setModel(@NotNull final Map> results) { + setModel(results, DEFAULT_SEVERITIES); } /** @@ -144,55 +157,101 @@ public void setModel(final Map> results) { * @param results the model. * @param levels the levels to display. */ - public void setModel(final Map> results, - final SeverityLevel... levels) { - visibleRootNode.removeAllChildren(); + public void setModel(@NotNull final Map> results, + @NotNull final Set levels) { + this.lastResults = results; + this.displayedSeverities = levels; + + rebuildTree(); + } - int itemCount = 0; - for (final PsiFile file : sortedFileNames(results)) { - final ToggleableTreeNode fileNode = new ToggleableTreeNode(); - final List problems = results.get(file); + private void groupResultsByFile() { + int problemCount = createFileNodes(sortByFileName(lastResults), visibleRootNode); + setRootMessage(problemCount); + } - int problemCount = 0; - if (problems != null) { - for (final Problem problem : problems) { - if (problem.severityLevel() != SeverityLevel.Ignore) { - final ResultTreeNode problemObj = new ProblemResultTreeNode(file, problem); + private List sortByFileName(final Map> results) { + if (results == null || results.isEmpty()) { + return emptyList(); + } + var sortedFiles = new ArrayList<>(results.keySet()); + sortedFiles.sort(comparing(PsiFileSystemItem::getName)); + return sortedFiles; + } - final ToggleableTreeNode problemNode = new ToggleableTreeNode(problemObj); - fileNode.add(problemNode); + private void groupResultsByPackage() { + int problemCount = 0; - ++problemCount; - } - } + var groupedByPackage = groupByPackageName(lastResults); + for (String packageName : groupedByPackage.keySet()) { + final var packageNode = new ToggleableTreeNode(); + + var childProblemCount = createFileNodes(groupedByPackage.getOrDefault(packageName, emptyList()), packageNode); + if (childProblemCount > 0) { + final var packageInfo = new PackageTreeInfo(packageName, childProblemCount); + packageNode.setUserObject(packageInfo); + visibleRootNode.add(packageNode); } - itemCount += problemCount; + problemCount += childProblemCount; + } + + setRootMessage(problemCount); + } + + private int createFileNodes(final List files, + final ToggleableTreeNode parentNode) { + int problemCount = 0; + for (final PsiFile file : files) { + final var fileNode = new ToggleableTreeNode(); + final var problems = lastResults.getOrDefault(file, emptyList()); + + int childProblemCount = 0; + for (final Problem problem : problems) { + if (problem.severityLevel() != SeverityLevel.Ignore) { + final var problemInfo = new ProblemResultTreeInfo(file, problem); + fileNode.add(new ToggleableTreeNode(problemInfo)); + + ++childProblemCount; + } + } - if (problemCount > 0) { - final ResultTreeNode nodeObject = new FileResultTreeNode(file.getName(), problemCount); + if (childProblemCount > 0) { + var nodeObject = new FileResultTreeInfo(file.getName(), childProblemCount); fileNode.setUserObject(nodeObject); - visibleRootNode.add(fileNode); + parentNode.add(fileNode); } - } - if (itemCount == 0) { - setRootMessage("plugin.results.scan-no-results"); - } else { - setRootText(CheckStyleBundle.message("plugin.results.scan-results", itemCount, results.size())); + problemCount += childProblemCount; } - - filter(false, levels); - nodeStructureChanged(visibleRootNode); + return problemCount; } - private Iterable sortedFileNames(final Map> results) { + private SortedMap> groupByPackageName(final Map> results) { if (results == null || results.isEmpty()) { - return Collections.emptyList(); + return Collections.emptySortedMap(); + } + var groupedByPackage = new TreeMap>(); + for (var result : results.keySet()) { + var filePackage = CheckStyleBundle.message("plugin.results.unknown-package"); + if (result instanceof PsiJavaFile javaFile) { + filePackage = javaFile.getPackageName(); + + if (filePackage.trim().isEmpty()) { + filePackage = CheckStyleBundle.message("plugin.results.root-package"); + } + } + groupedByPackage.computeIfAbsent(filePackage, key -> new ArrayList<>()).add(result); + } + return groupedByPackage; + } + + private void setRootMessage(final int problemCount) { + if (problemCount == 0) { + setRootMessage("plugin.results.scan-no-results"); + } else { + setRootText(CheckStyleBundle.message("plugin.results.scan-results", problemCount, lastResults.size())); } - final List sortedFiles = new ArrayList<>(results.keySet()); - sortedFiles.sort(comparing(PsiFileSystemItem::getName)); - return sortedFiles; } } diff --git a/src/main/java/org/infernus/idea/checkstyle/toolwindow/ResultTreeRenderer.java b/src/main/java/org/infernus/idea/checkstyle/toolwindow/ResultTreeRenderer.java index c538b8cf..866b475e 100644 --- a/src/main/java/org/infernus/idea/checkstyle/toolwindow/ResultTreeRenderer.java +++ b/src/main/java/org/infernus/idea/checkstyle/toolwindow/ResultTreeRenderer.java @@ -19,7 +19,6 @@ public class ResultTreeRenderer extends JLabel * Create a new cell renderer. */ public ResultTreeRenderer() { - super(); setOpaque(false); } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 2ca1e0eb..f3ea3787 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -25,6 +25,7 @@ +
  • 5.99.0: Added option to group results by package (#3).
  • 5.98.0: New: Added Checkstyle 10.20.1.
  • 5.97.0: Fixed: Refactored code to fix exception around API dependencies at initialisation (#655).
  • 5.97.0: New: Added Checkstyle 10.19.0.
  • 1 @@ -153,6 +154,20 @@ text="Display Information Results" description="Display information results" icon="/general/information.svg"/> + + + + + + diff --git a/src/main/resources/org/infernus/idea/checkstyle/CheckStyleBundle.properties b/src/main/resources/org/infernus/idea/checkstyle/CheckStyleBundle.properties index 75cbc899..5af2859d 100644 --- a/src/main/resources/org/infernus/idea/checkstyle/CheckStyleBundle.properties +++ b/src/main/resources/org/infernus/idea/checkstyle/CheckStyleBundle.properties @@ -19,10 +19,14 @@ plugin.results.error.instantiation-failed=The module {0} could not be loaded - \ please check you have added any prerequisites to the classpath plugin.results.scan-no-results=Checkstyle found no problems in the file(s) plugin.results.scan-results=Checkstyle found {0} item(s) in {1} file(s) +plugin.results.scan-package-result={0} : {1} item(s) +plugin.results.scan-package-result.filtered={0} : {1} item(s), {2} more hidden plugin.results.scan-file-result={0} : {1} item(s) plugin.results.scan-file-result.filtered={0} : {1} item(s), {2} more hidden plugin.results.file-result={1} ({2}:{3}) [{4}] plugin.results.unknown-source=unknown +plugin.results.unknown-package=Non-java file +plugin.results.root-package= plugin.status.in-progress.current=Scanning current file... plugin.status.in-progress.module=Scanning current module... plugin.status.in-progress.no-file=No file is open for editing