/*
* The MIT License (MIT)
*
* Copyright (c) 2017 hsz Jakub Chrzanowski <jakub@hsz.mobi>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package mobi.hsz.idea.gitignore.settings;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.ServiceManager;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.containers.ContainerUtil;
import mobi.hsz.idea.gitignore.IgnoreBundle;
import mobi.hsz.idea.gitignore.lang.IgnoreLanguage;
import mobi.hsz.idea.gitignore.util.Constants;
import mobi.hsz.idea.gitignore.util.Listenable;
import org.jdom.Element;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
/**
* Persistent global settings object for the Ignore plugin.
*
* @author Jakub Chrzanowski <jakub@hsz.mobi>
* @since 0.6.1
*/
@State(
name = "IgnoreSettings",
storages = @Storage(id = "other", file = "$APP_CONFIG$/ignore.xml")
)
public class IgnoreSettings implements PersistentStateComponent<Element>, Listenable<IgnoreSettings.Listener> {
/** Settings keys. */
public enum KEY {
ROOT("IgnoreSettings"), MISSING_GITIGNORE("missingGitignore"), USER_TEMPLATES("userTemplates"),
USER_TEMPLATES_TEMPLATE("template"), USER_TEMPLATES_NAME("name"), LANGUAGES("languages"),
LANGUAGES_LANGUAGE("language"), LANGUAGES_ID("id"), IGNORED_FILE_STATUS("ignoredFileStatus"),
OUTER_IGNORE_RULES("outerIgnoreRules"), OUTER_IGNORE_WRAPPER_HEIGHT("outerIgnoreWrapperHeight"),
INSERT_AT_CURSOR("insertAtCursor"), ADD_UNVERSIONED_FILES("addUnversionedFiles"), VERSION("version"),
STARRED_TEMPLATES("starredTemplates"), UNIGNORE_ACTIONS("unignoreActions"),
HIDE_IGNORED_FILES("hideIgnoredFiles"), INFORM_TRACKED_IGNORED("informTrackedIgnored"),
NOTIFY_IGNORED_EDITING("notifyIgnoredEditing");
private final String key;
KEY(@NotNull String key) {
this.key = key;
}
@Override
public String toString() {
return this.key;
}
}
/** Default user template. */
@NotNull
private static final UserTemplate DEFAULT_TEMPLATE = new UserTemplate(
IgnoreBundle.message("settings.userTemplates.default.name"),
IgnoreBundle.message("settings.userTemplates.default.content")
);
/** Notify about missing Gitignore file in the project. */
private boolean missingGitignore = true;
/** Enable ignored file status coloring. */
private boolean ignoredFileStatus = true;
/** Height of the outer ignore file wrapper panel. */
private int outerIgnoreWrapperHeight = 100;
/** Enable outer ignore rules. */
private boolean outerIgnoreRules = true;
/** Insert new entries at the cursor's position or at the document end. */
private boolean insertAtCursor = false;
/** Suggest to add unversioned files to the .gitignore file. */
private boolean addUnversionedFiles = true;
/** Plugin version. */
private String version;
/** Enable unignore actions in context menus. */
private boolean unignoreActions = true;
/** Hide ignored files or folder in the project tree view. */
private boolean hideIgnoredFiles = false;
/** Inform user about the tracked and ignored files in the project. */
private boolean informTrackedIgnored = true;
/** Shows notification about editing ignored file. */
private boolean notifyIgnoredEditing = true;
/** Starred templates. */
@NotNull
private final List<String> starredTemplates = ContainerUtil.newArrayList();
/** Settings related to the {@link IgnoreLanguage}. */
@NotNull
@SuppressWarnings("checkstyle:whitespacearound")
private IgnoreLanguagesSettings languagesSettings = new IgnoreLanguagesSettings() {{
for (final IgnoreLanguage language : IgnoreBundle.LANGUAGES) {
put(language, new TreeMap<KEY, Object>() {{
put(KEY.NEW_FILE, true);
put(KEY.ENABLE, language.isVCS());
}});
}
}};
/** Lists all user defined templates. */
private final List<UserTemplate> userTemplates = ContainerUtil.newArrayList(DEFAULT_TEMPLATE);
/** Listeners list. */
private final List<Listener> listeners = ContainerUtil.newArrayList();
/**
* Get the instance of this service.
*
* @return the unique {@link IgnoreSettings} instance.
*/
public static IgnoreSettings getInstance() {
return ServiceManager.getService(IgnoreSettings.class);
}
/**
* Get the settings state as a DOM element.
*
* @return an ready to serialize DOM {@link Element}.
*
* @see {@link #loadState(Element)}
*/
@Nullable
@Override
public Element getState() {
final Element element = new Element(KEY.ROOT.toString());
element.setAttribute(KEY.MISSING_GITIGNORE.toString(), Boolean.toString(missingGitignore));
element.setAttribute(KEY.IGNORED_FILE_STATUS.toString(), Boolean.toString(ignoredFileStatus));
element.setAttribute(KEY.OUTER_IGNORE_RULES.toString(), Boolean.toString(outerIgnoreRules));
element.setAttribute(KEY.OUTER_IGNORE_WRAPPER_HEIGHT.toString(), Integer.toString(outerIgnoreWrapperHeight));
element.setAttribute(KEY.VERSION.toString(), version);
element.setAttribute(KEY.STARRED_TEMPLATES.toString(), StringUtil.join(starredTemplates, Constants.DOLLAR));
element.setAttribute(KEY.UNIGNORE_ACTIONS.toString(), Boolean.toString(unignoreActions));
element.setAttribute(KEY.HIDE_IGNORED_FILES.toString(), Boolean.toString(hideIgnoredFiles));
element.setAttribute(KEY.INFORM_TRACKED_IGNORED.toString(), Boolean.toString(informTrackedIgnored));
element.setAttribute(KEY.NOTIFY_IGNORED_EDITING.toString(), Boolean.toString(notifyIgnoredEditing));
Element languagesElement = new Element(KEY.LANGUAGES.toString());
for (Map.Entry<IgnoreLanguage, TreeMap<IgnoreLanguagesSettings.KEY, Object>> entry :
languagesSettings.entrySet()) {
if (entry.getKey() == null) {
continue;
}
Element languageElement = new Element(KEY.LANGUAGES_LANGUAGE.toString());
languageElement.setAttribute(KEY.LANGUAGES_ID.toString(), entry.getKey().getID());
for (Map.Entry<IgnoreLanguagesSettings.KEY, Object> data : entry.getValue().entrySet()) {
languageElement.setAttribute(data.getKey().name(), data.getValue().toString());
}
languagesElement.addContent(languageElement);
}
element.addContent(languagesElement);
element.addContent(createTemplatesElement(userTemplates));
return element;
}
/**
* Creates {@link Element} with a list of the {@link UserTemplate} items.
*
* @param userTemplates templates
* @return {@link Element} instance with user templates
*/
public static Element createTemplatesElement(@NotNull List<UserTemplate> userTemplates) {
Element templates = new Element(KEY.USER_TEMPLATES.toString());
for (UserTemplate userTemplate : userTemplates) {
Element templateElement = new Element(KEY.USER_TEMPLATES_TEMPLATE.toString());
templateElement.setAttribute(KEY.USER_TEMPLATES_NAME.toString(), userTemplate.getName());
templateElement.addContent(userTemplate.getContent());
templates.addContent(templateElement);
}
return templates;
}
/**
* Load the settings state from the DOM {@link Element}.
*
* @param element the {@link Element} to load values from.
* @see {@link #getState()}
*/
@Override
public void loadState(Element element) {
String value = element.getAttributeValue(KEY.MISSING_GITIGNORE.toString());
if (value != null) {
missingGitignore = Boolean.parseBoolean(value);
}
value = element.getAttributeValue(KEY.IGNORED_FILE_STATUS.toString());
if (value != null) {
ignoredFileStatus = Boolean.parseBoolean(value);
}
value = element.getAttributeValue(KEY.OUTER_IGNORE_RULES.toString());
if (value != null) {
outerIgnoreRules = Boolean.parseBoolean(value);
}
value = element.getAttributeValue(KEY.VERSION.toString());
if (value != null) {
version = value;
}
value = element.getAttributeValue(KEY.OUTER_IGNORE_WRAPPER_HEIGHT.toString());
if (value != null) {
outerIgnoreWrapperHeight = Integer.parseInt(value);
}
value = element.getAttributeValue(KEY.STARRED_TEMPLATES.toString());
if (value != null) {
setStarredTemplates(StringUtil.split(value, Constants.DOLLAR));
}
value = element.getAttributeValue(KEY.HIDE_IGNORED_FILES.toString());
if (value != null) {
hideIgnoredFiles = Boolean.parseBoolean(value);
}
value = element.getAttributeValue(KEY.INFORM_TRACKED_IGNORED.toString());
if (value != null) {
informTrackedIgnored = Boolean.parseBoolean(value);
}
value = element.getAttributeValue(KEY.NOTIFY_IGNORED_EDITING.toString());
if (value != null) {
notifyIgnoredEditing = Boolean.parseBoolean(value);
}
Element languagesElement = element.getChild(KEY.LANGUAGES.toString());
if (languagesElement != null) {
for (Element languageElement : languagesElement.getChildren()) {
TreeMap<IgnoreLanguagesSettings.KEY, Object> data = ContainerUtil.newTreeMap();
for (IgnoreLanguagesSettings.KEY key : IgnoreLanguagesSettings.KEY.values()) {
data.put(key, languageElement.getAttributeValue(key.name()));
}
String id = languageElement.getAttributeValue(KEY.LANGUAGES_ID.toString());
IgnoreLanguage language = IgnoreBundle.LANGUAGES.get(id);
languagesSettings.put(language, data);
}
}
value = element.getAttributeValue(KEY.UNIGNORE_ACTIONS.toString());
if (value != null) {
unignoreActions = Boolean.parseBoolean(value);
}
userTemplates.clear();
userTemplates.addAll(loadTemplates(element));
for (IgnoreLanguage language : IgnoreBundle.LANGUAGES) {
if (!language.isVCS()) {
languagesSettings.get(language).put(IgnoreLanguagesSettings.KEY.ENABLE, false);
}
}
}
/**
* Loads {@link UserTemplate} objects from the {@link Element}.
*
* @param element source
* @return {@link UserTemplate} list
*/
@NotNull
public static List<UserTemplate> loadTemplates(@NotNull Element element) {
final String key = KEY.USER_TEMPLATES.toString();
final List<UserTemplate> list = ContainerUtil.newArrayList();
if (!key.equals(element.getName())) {
element = element.getChild(key);
}
for (Element template : element.getChildren()) {
list.add(new UserTemplate(
template.getAttributeValue(KEY.USER_TEMPLATES_NAME.toString()),
template.getText()
));
}
return list;
}
/**
* Notify about missing Gitignore file in the project.
*
* @return {@link #missingGitignore}
*/
public boolean isMissingGitignore() {
return missingGitignore;
}
/**
* Notify about missing Gitignore file in the project.
*
* @param missingGitignore notify about missing Gitignore file in the project
*/
public void setMissingGitignore(boolean missingGitignore) {
this.notifyOnChange(KEY.MISSING_GITIGNORE, this.missingGitignore, missingGitignore);
this.missingGitignore = missingGitignore;
}
/**
* Check if ignored file status coloring is enabled.
*
* @return ignored file status coloring is enabled
*/
public boolean isIgnoredFileStatus() {
return ignoredFileStatus;
}
/**
* Sets ignored file status coloring.
*
* @param ignoredFileStatus ignored file status coloring
*/
public void setIgnoredFileStatus(boolean ignoredFileStatus) {
this.notifyOnChange(KEY.IGNORED_FILE_STATUS, this.ignoredFileStatus, ignoredFileStatus);
this.ignoredFileStatus = ignoredFileStatus;
}
/**
* Check if outer ignore rules is enabled.
*
* @return ignored file status coloring is enabled
*/
public boolean isOuterIgnoreRules() {
return outerIgnoreRules;
}
/**
* Sets outer ignore rules.
*
* @param outerIgnoreRules ignored file status coloring
*/
public void setOuterIgnoreRules(boolean outerIgnoreRules) {
this.notifyOnChange(KEY.OUTER_IGNORE_RULES, this.outerIgnoreRules, outerIgnoreRules);
this.outerIgnoreRules = outerIgnoreRules;
}
/**
* Check if new entries should be inserted at the cursor's position or at the document end.
*
* @return entries should be inserted at the cursor's position
*/
public boolean isInsertAtCursor() {
return insertAtCursor;
}
/**
* Defines that new entries should be inserted at the cursor's position or at the document end.
*
* @param insertAtCursor position
*/
public void setInsertAtCursor(boolean insertAtCursor) {
this.notifyOnChange(KEY.INSERT_AT_CURSOR, this.insertAtCursor, insertAtCursor);
this.insertAtCursor = insertAtCursor;
}
/**
* Check if suggesting of adding unversioned files to the .gitignore file is enabled.
*
* @return entries should be inserted at the cursor's position
*/
public boolean isAddUnversionedFiles() {
return addUnversionedFiles;
}
/**
* Sets suggesting of adding unversioned files to the .gitignore file.
*
* @param addUnversionedFiles suggest for .gitignore files
*/
public void setAddUnversionedFiles(boolean addUnversionedFiles) {
this.notifyOnChange(KEY.ADD_UNVERSIONED_FILES, this.addUnversionedFiles, addUnversionedFiles);
this.addUnversionedFiles = addUnversionedFiles;
}
/**
* Check if ignored files should be hidden in the project tree view.
*
* @return true if the files should be ignored and false if they should be showed
*/
public boolean isHideIgnoredFiles() {
return hideIgnoredFiles;
}
/**
* Changes the configuration to determine if ignored files should be hidden in the project tree view or not.
*
* @param hideIgnoredFiles should hide ignored files
*/
public void setHideIgnoredFiles(boolean hideIgnoredFiles) {
this.notifyOnChange(KEY.HIDE_IGNORED_FILES, this.hideIgnoredFiles, hideIgnoredFiles);
this.hideIgnoredFiles = hideIgnoredFiles;
}
/**
* Inform user about the tracked and ignored files in the project.
*
* @return true if the files should be ignored and false if they should be showed
*/
public boolean isInformTrackedIgnored() {
return informTrackedIgnored;
}
/**
* Sets notification about editing ignored file status to enabled or disabled.
*
* @param informTrackedIgnored show or hide notification
*/
public void setInformTrackedIgnored(boolean informTrackedIgnored) {
this.notifyOnChange(KEY.INFORM_TRACKED_IGNORED, this.informTrackedIgnored, informTrackedIgnored);
this.informTrackedIgnored = informTrackedIgnored;
}
/**
* Checks if notifications about editing ignored file are enabled
*
* @return true if notification are enabled
*/
public boolean isNotifyIgnoredEditing() {
return notifyIgnoredEditing;
}
/**
* Sets value for informing user about the tracked and ignored files in the project.
*
* @param notifyIgnoredEditing inform about files
*/
public void setNotifyIgnoredEditing(boolean notifyIgnoredEditing) {
this.notifyOnChange(KEY.NOTIFY_IGNORED_EDITING, this.notifyIgnoredEditing, notifyIgnoredEditing);
this.notifyIgnoredEditing = notifyIgnoredEditing;
}
/**
* Returns the height of the outer ignore file wrapper panel.
*
* @return wrapper panel height
*/
public int getOuterIgnoreWrapperHeight() {
return outerIgnoreWrapperHeight;
}
/**
* Sets outer ignore rules.
*
* @param outerIgnoreWrapperHeight wrapper panel height
*/
public void setOuterIgnoreWrapperHeight(int outerIgnoreWrapperHeight) {
this.notifyOnChange(KEY.OUTER_IGNORE_WRAPPER_HEIGHT, this.outerIgnoreWrapperHeight, outerIgnoreWrapperHeight);
this.outerIgnoreWrapperHeight = outerIgnoreWrapperHeight;
}
/**
* Returns plugin version.
*
* @return version
*/
public String getVersion() {
return version;
}
/**
* Sets plugin version.
*
* @param version of the plugin
*/
public void setVersion(@NotNull String version) {
this.notifyOnChange(KEY.VERSION, this.version, version);
this.version = version;
}
/**
* Returns starred templates list.
*
* @return starred templates
*/
@NotNull
public List<String> getStarredTemplates() {
return starredTemplates;
}
/**
* Clears current {@link #starredTemplates} lists and adds new elements.
*
* @param starredTemplates new templates list
*/
public void setStarredTemplates(@NotNull List<String> starredTemplates) {
this.starredTemplates.clear();
this.starredTemplates.addAll(starredTemplates);
}
/**
* Gets the {@link IgnoreLanguage} settings.
*
* @return fileType settings
*/
@NotNull
public IgnoreLanguagesSettings getLanguagesSettings() {
return languagesSettings;
}
/**
* Sets the {@link IgnoreLanguage} settings.
*
* @param languagesSettings languagesSettings
*/
public void setLanguagesSettings(@NotNull IgnoreLanguagesSettings languagesSettings) {
this.notifyOnChange(KEY.LANGUAGES, this.languagesSettings, languagesSettings);
this.languagesSettings.clear();
this.languagesSettings.putAll(languagesSettings);
}
/**
* Gets the list of user defined templates.
*
* @return user templates
*/
public List<UserTemplate> getUserTemplates() {
return userTemplates;
}
/**
* Sets the list of user defined templates.
*
* @param userTemplates user templates
*/
public void setUserTemplates(@NotNull List<UserTemplate> userTemplates) {
this.notifyOnChange(KEY.USER_TEMPLATES, this.userTemplates, userTemplates);
this.userTemplates.clear();
this.userTemplates.addAll(userTemplates);
}
/**
* Check if unignore actions group is enabled.
*
* @return unignore actions group is enabled
*/
public boolean isUnignoreActions() {
return unignoreActions;
}
/**
* Sets unignore actions group.
*
* @param unignoreActions unignore actions group
*/
public void setUnignoreActions(boolean unignoreActions) {
this.notifyOnChange(KEY.UNIGNORE_ACTIONS, this.unignoreActions, unignoreActions);
this.unignoreActions = unignoreActions;
}
/**
* Add the given listener. The listener will be executed in the containing instance's thread.
*
* @param listener listener to add
*/
@Override
public void addListener(@NotNull Listener listener) {
listeners.add(listener);
}
/**
* Remove the given listener.
*
* @param listener listener to remove
*/
@Override
public void removeListener(@NotNull Listener listener) {
listeners.remove(listener);
}
/**
* Notifies listeners about the changes.
*
* @param key changed property key
* @param oldValue new value
* @param newValue new value
*/
private void notifyOnChange(KEY key, Object oldValue, Object newValue) {
if (!newValue.equals(oldValue)) {
for (Listener listener : listeners) {
listener.onChange(key, newValue);
}
}
}
/** Listener interface for onChange event. */
public interface Listener {
void onChange(@NotNull KEY key, Object value);
}
/** User defined template model. */
public static class UserTemplate {
/** Template name. */
private String name = "";
/** Template content. */
private String content = "";
/** Constructor. */
public UserTemplate() {
}
/** Constructor. */
public UserTemplate(@NotNull String name, @NotNull String content) {
this.name = name;
this.content = content;
}
/**
* Sets template name.
*
* @param name template name
*/
public void setName(@NotNull String name) {
this.name = name;
}
/**
* Gets template name.
*
* @return template name
*/
public String getName() {
return name;
}
/**
* Sets template content.
*
* @param content template content
*/
public void setContent(@NotNull String content) {
this.content = content;
}
/**
* Gets template content.
*
* @return template content
*/
public String getContent() {
return content;
}
/**
* Returns a string representation of the object.
*
* @return string representation
*/
@Override
public String toString() {
return this.name;
}
/**
* Checks if template has set name or content.
*
* @return true if name or content is filled
*/
public boolean isEmpty() {
return this.name.isEmpty() && this.content.isEmpty();
}
/**
* Checks if objects are equal.
*
* @param obj another template
* @return templates are equal
*/
@Override
public boolean equals(Object obj) {
if (!(obj instanceof UserTemplate)) {
return false;
}
if (obj == this) {
return true;
}
UserTemplate t = (UserTemplate) obj;
return (getName() != null && getName().equals(t.getName()) || (getName() == null && t.getName() == null))
&& (getContent() != null && getContent().equals(t.getContent()) ||
(getContent() == null && t.getContent() == null));
}
}
/** Helper class for the {@link IgnoreLanguage} settings. */
public static class IgnoreLanguagesSettings
extends LinkedHashMap<IgnoreLanguage, TreeMap<IgnoreLanguagesSettings.KEY, Object>> {
/** Settings keys. */
public enum KEY {
NEW_FILE, ENABLE
}
/**
* Returns the value to which the specified key is mapped.
*
* @param language Ignore language
*/
public TreeMap<KEY, Object> get(IgnoreLanguage language) {
return super.get(language);
}
/**
* Returns a shallow copy of this <tt>HashMap</tt> instance: the keys and
* values themselves are not cloned.
*
* @return a shallow copy of this map
*/
@Override
public IgnoreLanguagesSettings clone() {
IgnoreLanguagesSettings copy = (IgnoreLanguagesSettings) super.clone();
for (Map.Entry<IgnoreLanguage, TreeMap<IgnoreLanguagesSettings.KEY, Object>> entry : copy.entrySet()) {
@SuppressWarnings("unchecked")
TreeMap<IgnoreLanguagesSettings.KEY, Object> data = (TreeMap<KEY, Object>) entry.getValue().clone();
copy.put(entry.getKey(), data);
}
return copy;
}
}
}