Skip to content

Commit

Permalink
Merge ce5d159 into 80b20c5
Browse files Browse the repository at this point in the history
  • Loading branch information
SeeSharpSoft committed May 11, 2019
2 parents 80b20c5 + ce5d159 commit ca4a6e3
Show file tree
Hide file tree
Showing 27 changed files with 481 additions and 46 deletions.
1 change: 1 addition & 0 deletions CHANGELOG
Expand Up @@ -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
Expand Down
14 changes: 14 additions & 0 deletions README.md
Expand Up @@ -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_
Expand Down Expand Up @@ -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_
Expand Down
Binary file added docs/contextmenu.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
@@ -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());
}
}
Expand Up @@ -8,13 +8,15 @@
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;
import com.intellij.psi.impl.source.DummyHolderFactory;
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;
Expand All @@ -27,14 +29,15 @@ public final class CsvHelper {

// replaces PsiElementFactory.SERVICE.getInstance(element.getProject()).createDummyHolder("<undefined>", 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 = "<undefined>";
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);
Expand All @@ -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) {
Expand Down
@@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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());
}
}
Expand Up @@ -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;
Expand Down
@@ -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<String> 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
}
}
@@ -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());
}
}
@@ -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;
}
}
@@ -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());
}
}
@@ -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<CsvFileAttributes> {

public Map<String, Attribute> 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;
}
}

0 comments on commit ca4a6e3

Please sign in to comment.