diff --git a/CHANGELOG b/CHANGELOG index 1fd4fd51..adfdde64 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,6 +3,7 @@ May 7, 2019 NEW: option to keep/ignore a linebreak at the end of a file (table editor) NEW: improved change detection of table editor to avoid overwriting original text representation without editing any values +NEW: file based value separator (e.g. ',' or ';') 2.3.1 Mar 31, 2019 diff --git a/README.md b/README.md index 658627a7..801e7e36 100644 --- a/README.md +++ b/README.md @@ -207,6 +207,8 @@ Annasusanna,Amsterdam,1 The following separators are currently supported: **,** (Comma), **;** (Semicolon), **|** (Pipe) and **↹** (Tab) +_Value separator (default)_ defines which separator is used by default. The separator character can be changed for each CSV file individually. + When changing the separator, press the apply button to refresh the preview window properly. _Space before separator_ @@ -279,6 +281,18 @@ Annasusanna,Amsterdam, 1 Ben, Berlin, 2 ``` +### Actions + +#### File specific value separator + +![Context menu](./docs/contextmenu.png) + +The action to switch the value separator used for CSV syntax validation of a specific file is part of its text editors context menu. + + +This action defines how the parser/validator/highlighter/etc. behaves. It does intentionally not change the file content. +To be more precise: It **does not replace** previous separator characters by new ones or adjust the escaped texts. + ### Inspections - _File > Settings > Editor > Inspections > CSV_ diff --git a/docs/contextmenu.png b/docs/contextmenu.png new file mode 100644 index 00000000..78b89d24 Binary files /dev/null and b/docs/contextmenu.png differ diff --git a/src/main/java/net/seesharpsoft/intellij/lang/FileParserDefinition.java b/src/main/java/net/seesharpsoft/intellij/lang/FileParserDefinition.java new file mode 100644 index 00000000..a5da0a6d --- /dev/null +++ b/src/main/java/net/seesharpsoft/intellij/lang/FileParserDefinition.java @@ -0,0 +1,19 @@ +package net.seesharpsoft.intellij.lang; + +import com.intellij.lang.ParserDefinition; +import com.intellij.lang.PsiParser; +import com.intellij.lexer.Lexer; +import com.intellij.psi.PsiFile; + +/** + * Support for file specific parser definition. + */ +public interface FileParserDefinition extends ParserDefinition { + default Lexer createLexer(PsiFile file) { + return createLexer(file.getProject()); + } + + default PsiParser createParser(PsiFile file) { + return createParser(file.getProject()); + } +} diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvHelper.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvHelper.java index 64c4c08a..c4096dbb 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvHelper.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvHelper.java @@ -8,6 +8,7 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; import com.intellij.psi.PsiManager; import com.intellij.psi.TokenType; import com.intellij.psi.impl.source.DummyHolder; @@ -15,6 +16,7 @@ import com.intellij.psi.impl.source.tree.FileElement; import com.intellij.psi.tree.IElementType; import com.intellij.psi.util.PsiTreeUtil; +import net.seesharpsoft.intellij.lang.FileParserDefinition; import net.seesharpsoft.intellij.plugins.csv.psi.CsvField; import net.seesharpsoft.intellij.plugins.csv.psi.CsvFile; import net.seesharpsoft.intellij.plugins.csv.psi.CsvRecord; @@ -27,14 +29,15 @@ public final class CsvHelper { // replaces PsiElementFactory.SERVICE.getInstance(element.getProject()).createDummyHolder("", CsvTypes.FIELD, null); // https://github.com/SeeSharpSoft/intellij-csv-validator/issues/4 - public static PsiElement createEmptyCsvField(Project project) { + public static PsiElement createEmptyCsvField(PsiFile psiFile) { + final Project project = psiFile.getProject(); final String text = ""; final IElementType type = CsvTypes.FIELD; final PsiManager psiManager = PsiManager.getInstance(project); final DummyHolder dummyHolder = DummyHolderFactory.createHolder(psiManager, null); final FileElement fileElement = dummyHolder.getTreeElement(); - final ParserDefinition parserDefinition = LanguageParserDefinitions.INSTANCE.forLanguage(CsvLanguage.INSTANCE); - final Lexer lexer = parserDefinition.createLexer(project); + final FileParserDefinition parserDefinition = (FileParserDefinition)LanguageParserDefinitions.INSTANCE.forLanguage(CsvLanguage.INSTANCE); + final Lexer lexer = parserDefinition.createLexer(psiFile); final PsiBuilder psiBuilder = PsiBuilderFactory.getInstance().createBuilder(project, fileElement, lexer, CsvLanguage.INSTANCE, text); final ASTNode node = parserDefinition.createParser(project).parse(type, psiBuilder); fileElement.rawAddChildren((com.intellij.psi.impl.source.tree.TreeElement) node); @@ -44,7 +47,7 @@ public static PsiElement createEmptyCsvField(Project project) { public static boolean isCsvFile(Project project, VirtualFile file) { final FileType fileType = file.getFileType(); return (fileType instanceof LanguageFileType && ((LanguageFileType) fileType).getLanguage().isKindOf(CsvLanguage.INSTANCE)) || - (fileType == ScratchFileType.INSTANCE && LanguageUtil.getLanguageForPsi(project, file) == CsvLanguage.INSTANCE); + (fileType == ScratchFileType.INSTANCE && LanguageUtil.getLanguageForPsi(project, file).isKindOf(CsvLanguage.INSTANCE)); } public static IElementType getElementType(PsiElement element) { diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvParserDefinition.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvParserDefinition.java index e4553352..a4eecc28 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvParserDefinition.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvParserDefinition.java @@ -1,7 +1,6 @@ package net.seesharpsoft.intellij.plugins.csv; import com.intellij.lang.ASTNode; -import com.intellij.lang.ParserDefinition; import com.intellij.lang.PsiParser; import com.intellij.lexer.Lexer; import com.intellij.openapi.project.Project; @@ -11,21 +10,23 @@ import com.intellij.psi.TokenType; import com.intellij.psi.tree.IFileElementType; import com.intellij.psi.tree.TokenSet; +import net.seesharpsoft.intellij.lang.FileParserDefinition; import net.seesharpsoft.intellij.plugins.csv.parser.CsvParser; import net.seesharpsoft.intellij.plugins.csv.psi.CsvFile; +import net.seesharpsoft.intellij.plugins.csv.psi.CsvFileElementType; import net.seesharpsoft.intellij.plugins.csv.psi.CsvTypes; import net.seesharpsoft.intellij.plugins.csv.settings.CsvCodeStyleSettings; import org.jetbrains.annotations.NotNull; -public class CsvParserDefinition implements ParserDefinition { +public class CsvParserDefinition implements FileParserDefinition { public static final TokenSet WHITE_SPACES = TokenSet.create(TokenType.WHITE_SPACE); - public static final IFileElementType FILE = new IFileElementType(CsvLanguage.INSTANCE); + public static final IFileElementType FILE = new CsvFileElementType(CsvLanguage.INSTANCE); @NotNull @Override public Lexer createLexer(Project project) { - return new CsvLexerAdapter(CsvCodeStyleSettings.getCurrentSeparator(project)); + throw new UnsupportedOperationException("use 'createLexer(PsiFile file)' instead"); } @Override @@ -72,4 +73,14 @@ public SpaceRequirements spaceExistanceTypeBetweenTokens(ASTNode left, ASTNode r public PsiElement createElement(ASTNode node) { return CsvTypes.Factory.createElement(node); } + + @Override + public Lexer createLexer(@NotNull PsiFile file) { + return new CsvLexerAdapter(CsvCodeStyleSettings.getCurrentSeparator(file)); + } + + @Override + public PsiParser createParser(@NotNull PsiFile file) { + return createParser(file.getProject()); + } } \ No newline at end of file diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvParserUtil.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvParserUtil.java index b64ad1f7..b0e4847a 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvParserUtil.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvParserUtil.java @@ -15,8 +15,11 @@ private CsvParserUtil() { public static boolean separator(PsiBuilder builder, int tokenType) { if (builder.getTokenType() == CsvTypes.COMMA) { PsiFile currentFile = builder.getUserDataUnprotected(FileContextUtil.CONTAINING_FILE_KEY); + if (currentFile == null) { + throw new UnsupportedOperationException("parser requires containing file"); + } return builder.getTokenText().equals( - CsvCodeStyleSettings.getCurrentSeparator(builder.getProject(), currentFile != null ? currentFile.getLanguage() : null) + CsvCodeStyleSettings.getCurrentSeparator(currentFile) ); } return false; diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvStorageHelper.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvStorageHelper.java new file mode 100644 index 00000000..79b4dcb7 --- /dev/null +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvStorageHelper.java @@ -0,0 +1,29 @@ +package net.seesharpsoft.intellij.plugins.csv; + +import com.intellij.openapi.util.Key; +import com.intellij.psi.PsiFile; +import com.intellij.util.PathUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.regex.Pattern; + +public final class CsvStorageHelper { + public static final String CSV_STATE_STORAGE_FILE = "csv-plugin.xml"; + + public static final Key RELATIVE_FILE_URL = Key.create("CSV_PLUGIN_RELATIVE_URL"); + + public static String getRelativeFileUrl(@NotNull PsiFile psiFile) { + String url = psiFile.getUserData(RELATIVE_FILE_URL); + if (url == null) { + String projectDir = PathUtil.getLocalPath(psiFile.getProject().getBasePath()); + url = PathUtil.getLocalPath(psiFile.getOriginalFile().getVirtualFile().getPath()) + .replaceFirst("^" + Pattern.quote(projectDir), ""); + psiFile.putUserData(RELATIVE_FILE_URL, url); + } + return url; + } + + private CsvStorageHelper() { + // static + } +} diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/actions/CsvChangeSeparatorAction.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/actions/CsvChangeSeparatorAction.java new file mode 100644 index 00000000..e83717a9 --- /dev/null +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/actions/CsvChangeSeparatorAction.java @@ -0,0 +1,48 @@ +package net.seesharpsoft.intellij.plugins.csv.actions; + +import com.intellij.lang.Language; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.ToggleAction; +import com.intellij.openapi.components.ServiceManager; +import com.intellij.psi.PsiFile; +import com.intellij.util.FileContentUtil; +import net.seesharpsoft.intellij.plugins.csv.CsvLanguage; +import net.seesharpsoft.intellij.plugins.csv.CsvSeparatorHolder; +import net.seesharpsoft.intellij.plugins.csv.components.CsvFileAttributes; +import net.seesharpsoft.intellij.plugins.csv.settings.CsvCodeStyleSettings; +import org.jetbrains.annotations.NotNull; + +public class CsvChangeSeparatorAction extends ToggleAction { + private String mySeparator; + + CsvChangeSeparatorAction(String separator, String mySeparatorTextArg) { + super(mySeparatorTextArg); + mySeparator = separator; + } + + @Override + public boolean isSelected(@NotNull AnActionEvent anActionEvent) { + PsiFile psiFile = anActionEvent.getData(CommonDataKeys.PSI_FILE); + if (psiFile == null) { + return false; + } + CsvFileAttributes csvFileAttributes = ServiceManager.getService(psiFile.getProject(), CsvFileAttributes.class); + return csvFileAttributes.getFileSeparator(psiFile) != null && CsvCodeStyleSettings.getCurrentSeparator(psiFile).equals(mySeparator); + } + + @Override + public void setSelected(@NotNull AnActionEvent anActionEvent, boolean b) { + PsiFile psiFile = anActionEvent.getData(CommonDataKeys.PSI_FILE); + if (psiFile == null) { + return; + } + Language language = psiFile.getLanguage(); + if (!language.isKindOf(CsvLanguage.INSTANCE) || language instanceof CsvSeparatorHolder) { + return; + } + CsvFileAttributes csvFileAttributes = ServiceManager.getService(psiFile.getProject(), CsvFileAttributes.class); + csvFileAttributes.setFileSeparator(psiFile, this.mySeparator); + FileContentUtil.reparseFiles(psiFile.getVirtualFile()); + } + } \ No newline at end of file diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/actions/CsvChangeSeparatorActionGroup.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/actions/CsvChangeSeparatorActionGroup.java new file mode 100644 index 00000000..8dc4455a --- /dev/null +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/actions/CsvChangeSeparatorActionGroup.java @@ -0,0 +1,46 @@ +package net.seesharpsoft.intellij.plugins.csv.actions; + +import com.intellij.lang.Language; +import com.intellij.openapi.actionSystem.ActionGroup; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.psi.PsiFile; +import net.seesharpsoft.intellij.plugins.csv.CsvLanguage; +import net.seesharpsoft.intellij.plugins.csv.CsvSeparatorHolder; +import net.seesharpsoft.intellij.plugins.csv.settings.CsvCodeStyleSettings; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class CsvChangeSeparatorActionGroup extends ActionGroup { + + private static final AnAction[] CSV_SEPARATOR_CHANGE_ACTIONS; + + static { + CSV_SEPARATOR_CHANGE_ACTIONS = new AnAction[CsvCodeStyleSettings.SUPPORTED_SEPARATORS.length + 1]; + for (int i = 0; i < CSV_SEPARATOR_CHANGE_ACTIONS.length - 1; ++i) { + CSV_SEPARATOR_CHANGE_ACTIONS[i] = new CsvChangeSeparatorAction(CsvCodeStyleSettings.SUPPORTED_SEPARATORS[i], CsvCodeStyleSettings.SUPPORTED_SEPARATORS_DISPLAY[i]); + } + CSV_SEPARATOR_CHANGE_ACTIONS[CSV_SEPARATOR_CHANGE_ACTIONS.length - 1] = new CsvDefaultSeparatorAction(); + } + + @Override + public void update(AnActionEvent anActionEvent) { + PsiFile psiFile = anActionEvent.getData(CommonDataKeys.PSI_FILE); + Language language = psiFile == null ? null : psiFile.getLanguage(); + anActionEvent.getPresentation().setEnabledAndVisible(psiFile != null && language != null && + language.isKindOf(CsvLanguage.INSTANCE) && !(language instanceof CsvSeparatorHolder) + ); + + if (psiFile != null) { + anActionEvent.getPresentation() + .setText(String.format("CSV Value Separator: %s", CsvCodeStyleSettings.getSeparatorDisplayText(CsvCodeStyleSettings.getCurrentSeparator(psiFile)))); + } + } + + @NotNull + @Override + public AnAction[] getChildren(@Nullable AnActionEvent anActionEvent) { + return CSV_SEPARATOR_CHANGE_ACTIONS; + } +} diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/actions/CsvDefaultSeparatorAction.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/actions/CsvDefaultSeparatorAction.java new file mode 100644 index 00000000..e57c5251 --- /dev/null +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/actions/CsvDefaultSeparatorAction.java @@ -0,0 +1,44 @@ +package net.seesharpsoft.intellij.plugins.csv.actions; + +import com.intellij.lang.Language; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.actionSystem.ToggleAction; +import com.intellij.openapi.components.ServiceManager; +import com.intellij.psi.PsiFile; +import com.intellij.util.FileContentUtil; +import net.seesharpsoft.intellij.plugins.csv.CsvLanguage; +import net.seesharpsoft.intellij.plugins.csv.CsvSeparatorHolder; +import net.seesharpsoft.intellij.plugins.csv.components.CsvFileAttributes; +import org.jetbrains.annotations.NotNull; + +public class CsvDefaultSeparatorAction extends ToggleAction { + CsvDefaultSeparatorAction() { + super("Project Default"); + } + + @Override + public boolean isSelected(@NotNull AnActionEvent anActionEvent) { + PsiFile psiFile = anActionEvent.getData(CommonDataKeys.PSI_FILE); + if (psiFile == null) { + return false; + } + CsvFileAttributes csvFileAttributes = ServiceManager.getService(psiFile.getProject(), CsvFileAttributes.class); + return csvFileAttributes.getFileSeparator(psiFile) == null; + } + + @Override + public void setSelected(@NotNull AnActionEvent anActionEvent, boolean b) { + PsiFile psiFile = anActionEvent.getData(CommonDataKeys.PSI_FILE); + if (psiFile == null) { + return; + } + Language language = psiFile.getLanguage(); + if (!language.isKindOf(CsvLanguage.INSTANCE) || language instanceof CsvSeparatorHolder) { + return; + } + CsvFileAttributes csvFileAttributes = ServiceManager.getService(psiFile.getProject(), CsvFileAttributes.class); + csvFileAttributes.removeFileSeparator(psiFile); + FileContentUtil.reparseFiles(psiFile.getVirtualFile()); + } +} diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/components/CsvFileAttributes.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/components/CsvFileAttributes.java new file mode 100644 index 00000000..ecfccfef --- /dev/null +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/components/CsvFileAttributes.java @@ -0,0 +1,63 @@ +package net.seesharpsoft.intellij.plugins.csv.components; + +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; +import com.intellij.psi.PsiFile; +import com.intellij.util.xmlb.XmlSerializerUtil; +import net.seesharpsoft.intellij.plugins.csv.CsvStorageHelper; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.HashMap; +import java.util.Map; + +@State( + name = "CsvFileAttributes", + storages = {@Storage(CsvStorageHelper.CSV_STATE_STORAGE_FILE)} +) +@SuppressWarnings("all") +public class CsvFileAttributes implements PersistentStateComponent { + + public Map attributeMap = new HashMap<>(); + + static class Attribute { + public String separator; + } + + @Nullable + @Override + public CsvFileAttributes getState() { + return this; + } + + @Override + public void loadState(@NotNull CsvFileAttributes state) { + XmlSerializerUtil.copyBean(state, this); + } + + protected String generateMapKey(@NotNull PsiFile psiFile) { + return CsvStorageHelper.getRelativeFileUrl(psiFile); + } + + public void setFileSeparator(@NotNull PsiFile psiFile, @NotNull String separator) { + Attribute state = attributeMap.get(generateMapKey(psiFile)); + if (state == null) { + state = new Attribute(); + attributeMap.put(generateMapKey(psiFile), state); + } + state.separator = separator; + } + + public void removeFileSeparator(@NotNull PsiFile psiFile) { + attributeMap.remove(generateMapKey(psiFile)); + } + + public String getFileSeparator(@NotNull PsiFile psiFile) { + Attribute state = attributeMap.get(generateMapKey(psiFile)); + if (state != null) { + return state.separator; + } + return null; + } +} diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/CsvEditorSettingsExternalizable.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/CsvEditorSettingsExternalizable.java index 3b4af25d..53e7cd62 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/CsvEditorSettingsExternalizable.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/CsvEditorSettingsExternalizable.java @@ -6,6 +6,7 @@ import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; import com.intellij.openapi.editor.ex.EditorSettingsExternalizable; +import net.seesharpsoft.intellij.plugins.csv.CsvStorageHelper; import org.jetbrains.annotations.NotNull; import java.awt.*; @@ -14,7 +15,7 @@ @State( name = "CsvEditorSettings", - storages = {@Storage("csv-plugin.xml")} + storages = {@Storage(CsvStorageHelper.CSV_STATE_STORAGE_FILE)} ) @SuppressWarnings("all") public class CsvEditorSettingsExternalizable implements PersistentStateComponent { diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/CsvTableEditor.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/CsvTableEditor.java index 4ae6c5e5..992ee6a5 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/CsvTableEditor.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/CsvTableEditor.java @@ -44,12 +44,13 @@ public abstract class CsvTableEditor implements FileEditor, FileEditorLocation { protected final Project project; protected final VirtualFile file; protected final UserDataHolder userDataHolder; - protected final Document document; protected final PropertyChangeSupport changeSupport; - protected final PsiFile psiFile; - protected final String currentSeparator; protected final TableDataHandler dataManagement; + protected Document document; + protected PsiFile psiFile; + protected String currentSeparator; + private Object[][] initialState = null; private CsvTableEditorState storedState = null; @@ -59,11 +60,7 @@ public CsvTableEditor(@NotNull Project projectArg, @NotNull VirtualFile fileArg) this.project = projectArg; this.file = fileArg; this.userDataHolder = new UserDataHolderBase(); - this.document = FileDocumentManager.getInstance().getDocument(this.file); - PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project); - this.psiFile = documentManager.getPsiFile(this.document); this.changeSupport = new PropertyChangeSupport(this); - this.currentSeparator = CsvCodeStyleSettings.getCurrentSeparator(this.project, this.psiFile.getLanguage()); this.dataManagement = new TableDataHandler(this, TableDataHandler.MAX_SIZE); } @@ -83,7 +80,6 @@ public CsvTableEditor(@NotNull Project projectArg, @NotNull VirtualFile fileArg) public abstract int getPreferredRowHeight(); - public final void updateTableComponentData(Object[][] values) { beforeTableComponentUpdate(); try { @@ -289,6 +285,12 @@ public Project getProject() { @Nullable public CsvFile getCsvFile() { + if (this.psiFile == null || !this.psiFile.isValid()) { + this.document = FileDocumentManager.getInstance().getDocument(this.file); + PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project); + this.psiFile = documentManager.getPsiFile(this.document); + this.currentSeparator = CsvCodeStyleSettings.getCurrentSeparator(this.psiFile); + } return this.psiFile instanceof CsvFile ? (CsvFile) psiFile : null; } diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/swing/CsvTableEditorSwing.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/swing/CsvTableEditorSwing.java index 6244deeb..8f6db813 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/swing/CsvTableEditorSwing.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/swing/CsvTableEditorSwing.java @@ -143,7 +143,6 @@ private void initializedUIComponents() { applyEditorState(getFileEditorState()); rowHeadersTable = TableRowUtilities.addNumberColumn(tblEditor, 1); - } protected void applyTableChangeListener() { diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/psi/CsvFileElementType.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/psi/CsvFileElementType.java new file mode 100644 index 00000000..7a0a6c7a --- /dev/null +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/psi/CsvFileElementType.java @@ -0,0 +1,28 @@ +package net.seesharpsoft.intellij.plugins.csv.psi; + +import com.intellij.lang.*; +import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.tree.IFileElementType; +import net.seesharpsoft.intellij.lang.FileParserDefinition; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class CsvFileElementType extends IFileElementType { + public CsvFileElementType(@Nullable Language language) { + super(language); + } + + @Override + protected ASTNode doParseContents(@NotNull ASTNode chameleon, @NotNull PsiElement psi) { + PsiFile file = (PsiFile)psi; + Project project = file.getProject(); + Language languageForParser = this.getLanguageForParser(file); + FileParserDefinition parserDefinition = (FileParserDefinition) LanguageParserDefinitions.INSTANCE.forLanguage(languageForParser); + PsiBuilder builder = PsiBuilderFactory.getInstance().createBuilder(project, chameleon, parserDefinition.createLexer(file), languageForParser, chameleon.getChars()); + PsiParser parser = parserDefinition.createParser(file); + ASTNode node = parser.parse(this, builder); + return node.getFirstChildNode(); + } +} diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/settings/CsvCodeStyleSettings.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/settings/CsvCodeStyleSettings.java index 9aa2abf2..ba14bb4d 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/settings/CsvCodeStyleSettings.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/settings/CsvCodeStyleSettings.java @@ -2,11 +2,15 @@ import com.intellij.lang.Language; import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.project.Project; +import com.intellij.psi.PsiFile; import com.intellij.psi.codeStyle.CodeStyleSettings; import com.intellij.psi.codeStyle.CodeStyleSettingsManager; import com.intellij.psi.codeStyle.CustomCodeStyleSettings; +import com.intellij.util.ArrayUtil; import net.seesharpsoft.intellij.plugins.csv.CsvSeparatorHolder; +import net.seesharpsoft.intellij.plugins.csv.components.CsvFileAttributes; import org.jetbrains.annotations.Nullable; import java.util.regex.Pattern; @@ -55,6 +59,20 @@ public static String getCurrentSeparator(@Nullable Project project, @Nullable La return getCurrentSeparator(project); } + public static String getCurrentSeparator(@Nullable PsiFile psiFile) { + if (psiFile == null) { + return getCurrentSeparator((Project)null); + } + Project project = psiFile.getProject(); + CsvFileAttributes csvFileAttributes = project != null ? ServiceManager.getService(project, CsvFileAttributes.class) : null; + String separator = csvFileAttributes != null ? csvFileAttributes.getFileSeparator(psiFile) : null; + return separator != null ? separator : getCurrentSeparator(project, psiFile.getLanguage()); + } + + public static String getSeparatorDisplayText(String separator) { + return SUPPORTED_SEPARATORS_DISPLAY[ArrayUtil.find(SUPPORTED_SEPARATORS, separator)]; + } + public CsvCodeStyleSettings(CodeStyleSettings settings) { super("CsvCodeStyleSettings", settings); } diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/settings/CsvLanguageCodeStyleSettingsProvider.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/settings/CsvLanguageCodeStyleSettingsProvider.java index d562e491..3fbcc58f 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/settings/CsvLanguageCodeStyleSettingsProvider.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/settings/CsvLanguageCodeStyleSettingsProvider.java @@ -21,7 +21,7 @@ public void customizeSettings(@NotNull CodeStyleSettingsCustomizable consumer, @ if (settingsType == SettingsType.LANGUAGE_SPECIFIC) { consumer.showCustomOption(CsvCodeStyleSettings.class, "SEPARATOR_INDEX", - "Value separator", + "Value separator (default)", "Separator", CsvCodeStyleSettings.SUPPORTED_SEPARATORS_DISPLAY, IntStream.rangeClosed(0, CsvCodeStyleSettings.SUPPORTED_SEPARATORS.length - 1).toArray()); diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/structureview/CsvStructureViewElement.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/structureview/CsvStructureViewElement.java index 6781d6ad..9363c517 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/structureview/CsvStructureViewElement.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/structureview/CsvStructureViewElement.java @@ -109,7 +109,7 @@ public TreeElement[] getChildren() { CsvColumnInfo columnInfo = entry.getValue(); PsiElement psiElement = columnInfo.getHeaderElement(); if (psiElement == null) { - psiElement = CsvHelper.createEmptyCsvField(myElement.getProject()); + psiElement = CsvHelper.createEmptyCsvField(csvFile); } children[entry.getKey()] = new Header(psiElement, getElements(columnInfo, maxRowNumbers)); } @@ -139,7 +139,7 @@ public TreeElement[] getChildren() { TreeElement[] children = new TreeElement[getNumberOfChildren()]; for (PsiElement element : this.myElements) { if (rowIndex > 0) { - children[rowIndex - 1] = new Field(element == null ? CsvHelper.createEmptyCsvField(this.myElement.getProject()) : element, rowIndex - 1); + children[rowIndex - 1] = new Field(element == null ? CsvHelper.createEmptyCsvField(this.myElement.getContainingFile()) : element, rowIndex - 1); } ++rowIndex; } diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/tsv/TsvParserDefinition.java b/src/main/java/net/seesharpsoft/intellij/plugins/tsv/TsvParserDefinition.java index 5a841611..6f7fddf1 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/tsv/TsvParserDefinition.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/tsv/TsvParserDefinition.java @@ -1,24 +1,14 @@ package net.seesharpsoft.intellij.plugins.tsv; -import com.intellij.lexer.Lexer; -import com.intellij.openapi.project.Project; import com.intellij.psi.FileViewProvider; import com.intellij.psi.PsiFile; import com.intellij.psi.tree.IFileElementType; -import net.seesharpsoft.intellij.plugins.csv.CsvLexerAdapter; import net.seesharpsoft.intellij.plugins.csv.CsvParserDefinition; -import net.seesharpsoft.intellij.plugins.csv.settings.CsvCodeStyleSettings; import net.seesharpsoft.intellij.plugins.csv.psi.CsvFile; -import org.jetbrains.annotations.NotNull; +import net.seesharpsoft.intellij.plugins.csv.psi.CsvFileElementType; public class TsvParserDefinition extends CsvParserDefinition { - public static final IFileElementType TSV_FILE = new IFileElementType(TsvLanguage.INSTANCE); - - @NotNull - @Override - public Lexer createLexer(Project project) { - return new CsvLexerAdapter(CsvCodeStyleSettings.TAB_SEPARATOR); - } + public static final IFileElementType TSV_FILE = new CsvFileElementType(TsvLanguage.INSTANCE); @Override public IFileElementType getFileNodeType() { @@ -29,4 +19,4 @@ public IFileElementType getFileNodeType() { public PsiFile createFile(FileViewProvider viewProvider) { return new CsvFile(viewProvider, TsvFileType.INSTANCE); } -} \ No newline at end of file +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index d3e73260..e8bf6b87 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -48,6 +48,7 @@
 NEW: option to keep/ignore a linebreak at the end of a file (table editor)
 NEW: improved change detection of table editor to avoid overwriting original text representation without editing any values
+NEW: file based value separator (e.g. ',' or ';')
       
]]> @@ -63,26 +64,32 @@ NEW: improved change detection of table editor to avoid overwriting original tex - - + + - + - - - - + + + + + + + implementationClass="net.seesharpsoft.intellij.plugins.csv.spellchecker.CsvSpellCheckingStrategy"/> @@ -100,8 +107,9 @@ NEW: improved change detection of table editor to avoid overwriting original tex enabledByDefault="true" groupName="CSV" shortName="CsvValidation" - implementationClass="net.seesharpsoft.intellij.plugins.csv.inspection.CsvValidationInspection" /> - + implementationClass="net.seesharpsoft.intellij.plugins.csv.inspection.CsvValidationInspection"/> + net.seesharpsoft.intellij.plugins.csv.intention.CsvShiftColumnLeftIntentionAction @@ -136,6 +144,16 @@ NEW: improved change detection of table editor to avoid overwriting original tex + + + diff --git a/src/test/java/net/seesharpsoft/intellij/plugins/csv/actions/CsvChangeSeparatorActionTest.java b/src/test/java/net/seesharpsoft/intellij/plugins/csv/actions/CsvChangeSeparatorActionTest.java new file mode 100644 index 00000000..0acadfa4 --- /dev/null +++ b/src/test/java/net/seesharpsoft/intellij/plugins/csv/actions/CsvChangeSeparatorActionTest.java @@ -0,0 +1,66 @@ +package net.seesharpsoft.intellij.plugins.csv.actions; + +import com.intellij.openapi.actionSystem.Presentation; +import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase; +import net.seesharpsoft.intellij.plugins.csv.settings.CsvCodeStyleSettings; + +public class CsvChangeSeparatorActionTest extends LightCodeInsightFixtureTestCase { + + @Override + protected String getTestDataPath() { + return "./src/test/resources/actions"; + } + + public void testActionGroupVisibilityForCsv() { + myFixture.configureByFiles("CommaSeparated.csv"); + + Presentation presentation = myFixture.testAction(new CsvChangeSeparatorActionGroup()); + assertTrue(presentation.isVisible()); + assertTrue(presentation.isEnabled()); + } + + public void testActionGroupVisibilityForTsv() { + myFixture.configureByFiles("TabSeparated.tsv"); + + Presentation presentation = myFixture.testAction(new CsvChangeSeparatorActionGroup()); + assertFalse(presentation.isVisible()); + assertFalse(presentation.isEnabled()); + } + + public void testChangeSeparatorForCsv() { + myFixture.configureByFiles("CommaSeparated.csv"); + + for (int i = 0; i < CsvCodeStyleSettings.SUPPORTED_SEPARATORS.length; ++i) { + String newSeparator = CsvCodeStyleSettings.SUPPORTED_SEPARATORS[i]; + Presentation presentation = myFixture.testAction(new CsvChangeSeparatorAction(newSeparator, CsvCodeStyleSettings.getSeparatorDisplayText(newSeparator))); + assertEquals(CsvCodeStyleSettings.getSeparatorDisplayText(newSeparator), presentation.getText()); + assertEquals(newSeparator, CsvCodeStyleSettings.getCurrentSeparator(myFixture.getFile())); + } + } + + public void testChangeSeparatorForTsv() { + myFixture.configureByFiles("TabSeparated.tsv"); + + for (int i = 0; i < CsvCodeStyleSettings.SUPPORTED_SEPARATORS.length; ++i) { + String newSeparator = CsvCodeStyleSettings.SUPPORTED_SEPARATORS[i]; + Presentation presentation = myFixture.testAction(new CsvChangeSeparatorAction(newSeparator, CsvCodeStyleSettings.getSeparatorDisplayText(newSeparator))); + assertEquals(CsvCodeStyleSettings.getSeparatorDisplayText(newSeparator), presentation.getText()); + // for TSV files, the separator should always be a tab + assertEquals("\t", CsvCodeStyleSettings.getCurrentSeparator(myFixture.getFile())); + } + } + + public void testDefaultSeparatorAction() { + myFixture.configureByFiles("CommaSeparated.csv"); + + String initialSeparator = CsvCodeStyleSettings.getCurrentSeparator(myFixture.getFile()); + + myFixture.testAction(new CsvChangeSeparatorAction("|", CsvCodeStyleSettings.getSeparatorDisplayText("|"))); + + assertFalse("separator should not be initial", initialSeparator.equals(CsvCodeStyleSettings.getCurrentSeparator(myFixture.getFile()))); + + myFixture.testAction(new CsvDefaultSeparatorAction()); + + assertEquals(initialSeparator, CsvCodeStyleSettings.getCurrentSeparator(myFixture.getFile())); + } +} diff --git a/src/test/java/net/seesharpsoft/intellij/plugins/csv/settings/CsvCodeStyleSettingsTest.java b/src/test/java/net/seesharpsoft/intellij/plugins/csv/settings/CsvCodeStyleSettingsTest.java new file mode 100644 index 00000000..ae6a2877 --- /dev/null +++ b/src/test/java/net/seesharpsoft/intellij/plugins/csv/settings/CsvCodeStyleSettingsTest.java @@ -0,0 +1,21 @@ +package net.seesharpsoft.intellij.plugins.csv.settings; + +import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase; + +public class CsvCodeStyleSettingsTest extends LightCodeInsightFixtureTestCase { + + @Override + protected String getTestDataPath() { + return "./src/test/resources/settings"; + } + + public void testDefaultSeparator() { + assertEquals(CsvCodeStyleSettings.DEFAULT_SEPARATOR, CsvCodeStyleSettings.getCurrentSeparator(myFixture.getProject())); + } + + public void testFileDefaultSeparator() { + myFixture.configureByFiles("AnyFile.csv"); + + assertEquals(CsvCodeStyleSettings.DEFAULT_SEPARATOR, CsvCodeStyleSettings.getCurrentSeparator(myFixture.getFile())); + } +} diff --git a/src/test/java/net/seesharpsoft/intellij/plugins/tsv/intention/TsvInspectionTest.java b/src/test/java/net/seesharpsoft/intellij/plugins/tsv/inspection/TsvInspectionTest.java similarity index 96% rename from src/test/java/net/seesharpsoft/intellij/plugins/tsv/intention/TsvInspectionTest.java rename to src/test/java/net/seesharpsoft/intellij/plugins/tsv/inspection/TsvInspectionTest.java index e76a511f..1c03ee04 100644 --- a/src/test/java/net/seesharpsoft/intellij/plugins/tsv/intention/TsvInspectionTest.java +++ b/src/test/java/net/seesharpsoft/intellij/plugins/tsv/inspection/TsvInspectionTest.java @@ -1,4 +1,4 @@ -package net.seesharpsoft.intellij.plugins.tsv.intention; +package net.seesharpsoft.intellij.plugins.tsv.inspection; import com.intellij.codeInsight.intention.IntentionAction; import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase; diff --git a/src/test/resources/actions/CommaSeparated.csv b/src/test/resources/actions/CommaSeparated.csv new file mode 100644 index 00000000..37c37bce --- /dev/null +++ b/src/test/resources/actions/CommaSeparated.csv @@ -0,0 +1,2 @@ +this,is,"a test file; changing separator","will most likely fail ;)" +"just, another; pipe| line ",end diff --git a/src/test/resources/actions/TabSeparated.tsv b/src/test/resources/actions/TabSeparated.tsv new file mode 100644 index 00000000..688fde1f --- /dev/null +++ b/src/test/resources/actions/TabSeparated.tsv @@ -0,0 +1,2 @@ +this is "a test file; changing separator" "will most likely fail ;)" +"just, another; pipe| line " end diff --git a/src/test/resources/settings/AnyFile.csv b/src/test/resources/settings/AnyFile.csv new file mode 100644 index 00000000..e583e810 --- /dev/null +++ b/src/test/resources/settings/AnyFile.csv @@ -0,0 +1,7 @@ +1,"Eldon Base for stackable storage shelf, platinum", Muhammed MacIntyre ,3,-213.25 , 38.94 + 2 ," 1.7 Cubic Foot Compact ""Cube"" Office Refrigerators",Barry French, 293,457.81,208.16 + +3,"Cardinal Slant-D® Ring Binder, Heavy Gauge Vinyl ",Barry French, 293 ,46.71 ,8.69 +4 , R380 ,Clay Rozendal,483, 1198.97,195.99 +3.1 +5 ,Holmes HEPA Air Purifier,Carlos Soltero,515,30.94,21.78 \ No newline at end of file