Skip to content

Commit a7a304d

Browse files
authored
Merge pull request #175 from SeeSharpSoft/fb_flexible_escape_character
Customizable escape character
2 parents 9bc0096 + a1d28c3 commit a7a304d

File tree

53 files changed

+804
-277
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+804
-277
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Compatible with _IntelliJ IDEA PhpStorm WebStorm PyCharm RubyMine AppCode
1212
This plugin introduces CSV (_Comma-Separated Values_) as a language to Jetbrains IDE with a syntax definition, structured language elements and associated file types (.csv/.tsv/.psv).
1313
This enables default editor features like syntax validation, highlighting and inspections for CSV-alike files.
1414

15+
![CSV Plugin Example](./docs/example.png)
16+
1517
## Features
1618

1719
- CSV/TSV/PSV file detection
@@ -299,15 +301,15 @@ Annasusanna,Amsterdam, 1
299301

300302
### Actions
301303

302-
#### File specific value separator
304+
#### File specific value separator & escape character
303305

304306
![Context menu](./docs/contextmenu.png)
305307

306-
The action to switch the value separator used for CSV syntax validation of a specific file is part of its editors context menu.
308+
The action to switch the value separator (or escape character) - *which is used for CSV syntax validation of a specific file* - is part of its editors context menu.
307309

308310

309311
This action defines how the parser/validator/highlighter/etc. behaves. It does intentionally not change the file content.
310-
To be more precise: It **does not replace** previous separator characters by new ones or adjust the escaped texts.
312+
To be more precise: It **does not replace** previous separator/escape characters by new ones or adjust the escaped texts.
311313

312314
#### Adjust column widths (table editor only)
313315

docs/contextmenu.png

10.9 KB
Loading

docs/example.png

143 KB
Loading

src/main/java/net/seesharpsoft/intellij/plugins/csv/Csv.bnf

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
tokenTypeClass="net.seesharpsoft.intellij.plugins.csv.psi.CsvTokenType"
1616

1717
tokens=[
18-
TEXT='regexp:[^ ,;|\t\r\n"]+'
19-
ESCAPED_TEXT='regexp:([,;|\t\r\n]|"")+'
18+
TEXT='regexp:[^ ,;|\t\r\n"\\]+'
19+
ESCAPED_TEXT='regexp:[,;|\t\r\n\\]|""|\\"'
2020
COMMA='regexp:[,;|\t]'
2121
QUOTE='"'
2222
CRLF='regexp:\n'
@@ -29,6 +29,6 @@ record ::= field ( << separator >> COMMA field)*
2929

3030
field ::= (escaped | nonEscaped)
3131

32-
private escaped ::= QUOTE ( TEXT | ESCAPED_TEXT)* QUOTE
32+
private escaped ::= QUOTE ( TEXT | << escapeCharacter >> ESCAPED_TEXT)* QUOTE
3333

34-
private nonEscaped ::= TEXT*
34+
private nonEscaped ::= TEXT*

src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvHelper.java

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import com.intellij.psi.tree.IElementType;
1818
import com.intellij.psi.util.PsiTreeUtil;
1919
import net.seesharpsoft.intellij.lang.FileParserDefinition;
20+
import net.seesharpsoft.intellij.plugins.csv.components.CsvFileAttributes;
21+
import net.seesharpsoft.intellij.plugins.csv.editor.CsvEditorSettings;
2022
import net.seesharpsoft.intellij.plugins.csv.psi.CsvField;
2123
import net.seesharpsoft.intellij.plugins.csv.psi.CsvFile;
2224
import net.seesharpsoft.intellij.plugins.csv.psi.CsvRecord;
@@ -26,6 +28,7 @@
2628
import java.util.HashMap;
2729
import java.util.Map;
2830
import java.util.function.Function;
31+
import java.util.regex.Pattern;
2932

3033
public final class CsvHelper {
3134

@@ -149,14 +152,23 @@ public static int getFieldEndOffset(PsiElement field) {
149152
return separator == null ? field.getContainingFile().getTextLength() : separator.getTextOffset();
150153
}
151154

155+
public static CsvEditorSettings.EscapeCharacter getCurrentEscapeCharacter(CsvFile csvFile) {
156+
return getCurrentEscapeCharacter(csvFile.getContainingFile());
157+
}
158+
159+
public static CsvEditorSettings.EscapeCharacter getCurrentEscapeCharacter(PsiFile psiFile) {
160+
return CsvFileAttributes.getInstance(psiFile.getProject()).getEscapeCharacter(psiFile);
161+
}
162+
152163
public static CsvColumnInfoMap<PsiElement> createColumnInfoMap(CsvFile csvFile) {
164+
CsvEditorSettings.EscapeCharacter escapeCharacter = getCurrentEscapeCharacter(csvFile);
153165
Map<Integer, CsvColumnInfo<PsiElement>> columnInfoMap = new HashMap<>();
154166
CsvRecord[] records = PsiTreeUtil.getChildrenOfType(csvFile, CsvRecord.class);
155167
int row = 0;
156168
for (CsvRecord record : records) {
157169
int column = 0;
158170
for (CsvField field : record.getFieldList()) {
159-
Integer length = CsvHelper.getMaxTextLineLength(unquoteCsvValue(field.getText()));
171+
Integer length = CsvHelper.getMaxTextLineLength(unquoteCsvValue(field.getText(), escapeCharacter));
160172
if (!columnInfoMap.containsKey(column)) {
161173
columnInfoMap.put(column, new CsvColumnInfo(column, length, row));
162174
} else if (columnInfoMap.get(column).getMaxLength() < length) {
@@ -170,28 +182,28 @@ public static CsvColumnInfoMap<PsiElement> createColumnInfoMap(CsvFile csvFile)
170182
return new CsvColumnInfoMap(columnInfoMap, PsiTreeUtil.hasErrorElements(csvFile));
171183
}
172184

173-
public static String unquoteCsvValue(String content) {
185+
public static String unquoteCsvValue(String content, CsvEditorSettings.EscapeCharacter escapeCharacter) {
174186
if (content == null) {
175187
return "";
176188
}
177189
String result = content.trim();
178190
if (result.length() > 1 && result.startsWith("\"") && result.endsWith("\"")) {
179191
result = result.substring(1, result.length() - 1);
180192
}
181-
result = result.replaceAll("(?:\")\"", "\"");
193+
result = result.replaceAll("(?:" + Pattern.quote(escapeCharacter.getCharacter()) + ")\"", "\"");
182194
return result;
183195
}
184196

185197
private static boolean isQuotingRequired(String content, String separator) {
186198
return content != null && (content.contains(separator) || content.contains("\"") || content.contains("\n") || content.startsWith(" ") || content.endsWith(" "));
187199
}
188200

189-
public static String quoteCsvField(String content, String separator, boolean quotingEnforced) {
201+
public static String quoteCsvField(String content, CsvEditorSettings.EscapeCharacter escapeCharacter, String separator, boolean quotingEnforced) {
190202
if (content == null) {
191203
return "";
192204
}
193205
if (quotingEnforced || isQuotingRequired(content, separator)) {
194-
String result = content.replaceAll("\"", "\"\"");
206+
String result = content.replaceAll("\"", escapeCharacter.getCharacter() + "\"");
195207
return "\"" + result + "\"";
196208
}
197209
return content;

src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvLexer.flex

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@ package net.seesharpsoft.intellij.plugins.csv;
22

33
import com.intellij.lexer.FlexLexer;
44
import com.intellij.psi.tree.IElementType;
5-
import com.intellij.openapi.project.Project;
5+
import net.seesharpsoft.intellij.plugins.csv.editor.CsvEditorSettings;
66
import net.seesharpsoft.intellij.plugins.csv.psi.CsvTypes;
77
import com.intellij.psi.TokenType;
88

9+
import java.util.regex.Pattern;
10+
911
%%
1012

1113
%class CsvLexer
@@ -14,21 +16,25 @@ import com.intellij.psi.TokenType;
1416
%function advance
1517
%type IElementType
1618
%{
17-
private String currentSeparator;
18-
19-
/**
20-
* Provide constructor that supports a Project as parameter.
21-
*/
22-
CsvLexer(java.io.Reader in, String separator) {
23-
this(in);
24-
this.currentSeparator = separator;
25-
}
19+
private String currentSeparator;
20+
private CsvEditorSettings.EscapeCharacter myEscapeCharacter;
21+
22+
private static final Pattern ESCAPE_TEXT_PATTERN = Pattern.compile("[,;|\\t\\r\\n]");
23+
24+
/**
25+
* Provide constructor that supports a Project as parameter.
26+
*/
27+
CsvLexer(java.io.Reader in, String separator, CsvEditorSettings.EscapeCharacter escapeCharacter) {
28+
this(in);
29+
this.currentSeparator = separator;
30+
myEscapeCharacter = escapeCharacter;
31+
}
2632
%}
2733
%eof{ return;
2834
%eof}
2935

30-
TEXT=[^ ,;|\t\r\n\"]+
31-
ESCAPED_TEXT=([,;|\t\r\n]|\"\")+
36+
TEXT=[^ ,;|\t\r\n\"\\]+
37+
ESCAPED_TEXT=[,;|\t\r\n\\]|\"\"|\\\"
3238
QUOTE=\"
3339
COMMA=[,;|\t]
3440
EOL=\n
@@ -65,7 +71,13 @@ WHITE_SPACE=[ \f]+
6571

6672
<ESCAPED_TEXT> {ESCAPED_TEXT}
6773
{
68-
return CsvTypes.ESCAPED_TEXT;
74+
String text = yytext().toString();
75+
if (myEscapeCharacter.isEscapedQuote(text)
76+
|| ESCAPE_TEXT_PATTERN.matcher(text).matches()
77+
) {
78+
return CsvTypes.ESCAPED_TEXT;
79+
}
80+
return TokenType.BAD_CHARACTER;
6981
}
7082

7183
<YYINITIAL, AFTER_TEXT, UNESCAPED_TEXT> {COMMA}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
package net.seesharpsoft.intellij.plugins.csv;
22

33
import com.intellij.lexer.FlexAdapter;
4+
import net.seesharpsoft.intellij.plugins.csv.editor.CsvEditorSettings;
45

56
public class CsvLexerAdapter extends FlexAdapter {
6-
public CsvLexerAdapter(String separator) {
7-
super(new CsvLexer(null, separator));
7+
public CsvLexerAdapter(String separator, CsvEditorSettings.EscapeCharacter escapeCharacter) {
8+
super(new CsvLexer(null, separator, escapeCharacter));
89
}
910
}

src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvParserDefinition.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import com.intellij.psi.tree.IFileElementType;
1212
import com.intellij.psi.tree.TokenSet;
1313
import net.seesharpsoft.intellij.lang.FileParserDefinition;
14+
import net.seesharpsoft.intellij.plugins.csv.components.CsvFileAttributes;
1415
import net.seesharpsoft.intellij.plugins.csv.parser.CsvParser;
1516
import net.seesharpsoft.intellij.plugins.csv.psi.CsvFile;
1617
import net.seesharpsoft.intellij.plugins.csv.psi.CsvFileElementType;
@@ -76,11 +77,11 @@ public PsiElement createElement(ASTNode node) {
7677

7778
@Override
7879
public Lexer createLexer(@NotNull PsiFile file) {
79-
return new CsvLexerAdapter(CsvCodeStyleSettings.getCurrentSeparator(file));
80+
return new CsvLexerAdapter(CsvCodeStyleSettings.getCurrentSeparator(file), CsvFileAttributes.getInstance(file.getProject()).getEscapeCharacter(file));
8081
}
8182

8283
@Override
8384
public PsiParser createParser(@NotNull PsiFile file) {
8485
return createParser(file.getProject());
8586
}
86-
}
87+
}

src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvParserUtil.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,44 @@
33
import com.intellij.lang.PsiBuilder;
44
import com.intellij.psi.PsiFile;
55
import com.intellij.psi.impl.source.resolve.FileContextUtil;
6+
import net.seesharpsoft.intellij.plugins.csv.components.CsvFileAttributes;
67
import net.seesharpsoft.intellij.plugins.csv.psi.CsvTypes;
78
import net.seesharpsoft.intellij.plugins.csv.settings.CsvCodeStyleSettings;
89

10+
import java.util.regex.Pattern;
11+
912
public final class CsvParserUtil {
1013

14+
public static final Pattern ESCAPE_TEXT_PATTERN = Pattern.compile("[,;|\\t\\r\\n]");
15+
1116
private CsvParserUtil() {
1217
// static utility class
1318
}
1419

1520
public static boolean separator(PsiBuilder builder, int tokenType) {
1621
if (builder.getTokenType() == CsvTypes.COMMA) {
17-
PsiFile currentFile = builder.getUserDataUnprotected(FileContextUtil.CONTAINING_FILE_KEY);
18-
if (currentFile == null) {
22+
PsiFile psiFile = builder.getUserDataUnprotected(FileContextUtil.CONTAINING_FILE_KEY);
23+
if (psiFile == null) {
1924
throw new UnsupportedOperationException("parser requires containing file");
2025
}
2126
return builder.getTokenText().equals(
22-
CsvCodeStyleSettings.getCurrentSeparator(currentFile)
27+
CsvCodeStyleSettings.getCurrentSeparator(psiFile)
2328
);
2429
}
2530
return false;
2631
}
2732

33+
public static boolean escapeCharacter(PsiBuilder builder, int tokenType) {
34+
if (builder.getTokenType() == CsvTypes.ESCAPED_TEXT) {
35+
PsiFile psiFile = builder.getUserDataUnprotected(FileContextUtil.CONTAINING_FILE_KEY);
36+
if (psiFile == null) {
37+
throw new UnsupportedOperationException("parser requires containing file");
38+
}
39+
String tokenText = builder.getTokenText();
40+
return CsvFileAttributes.getInstance(psiFile.getProject()).getEscapeCharacter(psiFile).isEscapedQuote(tokenText) ||
41+
ESCAPE_TEXT_PATTERN.matcher(tokenText).matches();
42+
}
43+
return false;
44+
}
45+
2846
}

src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvStorageHelper.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public static String getRelativeFileUrl(Project project, VirtualFile virtualFile
1717
return null;
1818
}
1919
String url = virtualFile.getUserData(RELATIVE_FILE_URL);
20-
if (url == null) {
20+
if (url == null && project.getBasePath() != null) {
2121
String projectDir = PathUtil.getLocalPath(project.getBasePath());
2222
url = PathUtil.getLocalPath(virtualFile.getPath())
2323
.replaceFirst("^" + Pattern.quote(projectDir), "");

0 commit comments

Comments
 (0)