diff --git a/CHANGELOG.md b/CHANGELOG.md index 2bea676..b6700a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,24 @@ ### Fixed -## [4.1.0] - Sep 07, 2025 +## 4.2.0 - Jan 26, 2026 + +### Fixed + +- Compatibility verification issues +- Invalid PSI Element - class CsvFile because different providers +- PluginException - Element class CsvFile - file is invalid #923 +- NullPointerException #933 +- PluginException - Cannot check provider net.seesharpsoft.intellij.plugins.csv.editor.table.CsvTableEditorProvider #919 +- ExceptionInInitializerError #916 +- GithubStatusCodeException - 422 Unprocessable Entity - Validation Failed [Search; q]invalid: null #920 +- com.intellij.diagnostic.Freeze #921 +- Correct short name format +- Slow operations are prohibited on EDT #936 +- This method is forbidden on EDT because it does not pump the event queue #940 +- Argument for @NotNull parameter 'document' of PsiDocumentManagerImpl.getPsiFile must not be null #941 + +## 4.1.0 - Sep 07, 2025 ### Fixed @@ -22,7 +39,7 @@ - Project structure refactoring to avoid circular dependencies -## [4.0.2] - Dec 29, 2024 +## 4.0.2 - Dec 29, 2024 ### Added @@ -54,9 +71,11 @@ ## 4.0.0 - Oct 07, 2024 ### Added + - Tabularize formatting is back! ### Changed + - Text editor is shown first by default ## 3.4.0 - Aug 10, 2024 @@ -70,13 +89,6 @@ ### Fixed -- Update gradle build -- PluginException: xxx ms to call on EDT CsvChangeSeparatorActionGroup#update@EditorPopup #401 -- AlreadyDisposedException: Already disposed #639 -- Exceptions occurred on invoking the intention 'Unquote' on a copy of the file #670 #816 - -### Fixed - - StringIndexOutOfBoundsException: begin 0, end -1, length 5993 #801 - Unhandled exception in [CoroutineName(PsiAwareFileEditorManagerImpl)] #666 @@ -96,7 +108,6 @@ - NullPointerException: Cannot invoke "getSelectedColumn()" because "tblEditor" is null #519 - IllegalStateException: Attempt to modify PSI for non-committed Document! #516 - - StringIndexOutOfBoundsException: begin 0, end -1, length 5995 #511 - ArrayIndexOutOfBoundsException: 12 >= 12 #482 @@ -116,7 +127,6 @@ - Argument for @NotNull parameter 'tableEditor' of CsvTableActions.adjustColumnWidths must not be null #431 - NullPointerException #429 - - Comment handling breaking in table editor #451 ### Added @@ -136,30 +146,14 @@ - Argument for @NotNull parameter 'anchor' of CsvPsiTreeUpdater.appendField must not be null #392 - ArrayIndexOutOfBoundsException: 8 >= 8 #396 - - catch unreasonable exception when retrieving service #410 - NullPointerException: Cannot invoke "CsvTableEditor.getActions()" because the return value of "CsvTableEditorActions.getTableEditor(AnActionEvent)" is null #394 ### Added - 358 ms to call on EDT CsvChangeSeparatorActionGroup#update@EditorPopup #401 -## 3.0.2 - Dec 15, 2022 - -### Fixed - -- Detected bulk mode status update from DocumentBulkUpdateListener #384 -- Argument for @NotNull parameter 'parent' of PsiHelper.getNthChildOfType must not be null #372 -- Argument for @NotNull parameter 'element' of PsiHelper.getSiblingOfType must not be null #375 -- Cannot invoke "Document.getText()" because "document" is null #388 - -### Fixed - -- Cannot invoke "PsiFile.getProject()" because the return value of "CsvPsiTreeUpdater.getPsiFile()" is null #378 -- Argument for @NotNull parameter 'replacement' of CsvPsiTreeUpdater$ReplacePsiAction. must not be null #380 - -- provide project parameter for opening link -- Cannot invoke "Document.insertString(int, java.lang.CharSequence)" because "document" is null #386 +## 3.0.2 - Dec 15, 2022 ### Fixed @@ -169,13 +163,6 @@ ### Fixed -- cannot init component state (componentName=CsvFileAttributes) #359 -- cannot invoke "add(Object)" because "this.myUncommittedActions" is null #361 -- cannot invoke "createNotification(...)" because "notificationGroup" is null #362 -- cannot invoke "getManager()" because the return value of "getPsiFile()" is null #363 - -### Fixed - - image in plugin description - plugin update restart @@ -183,9 +170,6 @@ MAJOR UPDATE VERSION 3 -General -------- - - renamed plugin to 'CSV Editor' - fixed all compatibility issues with respect to IntelliJ platform 2022.* - rework language lexer @@ -194,10 +178,6 @@ General - adjusted setting dialogs - integrated GitHub issue reporter in case plugin raises an exception - removed TSV & PSV language, only CSV language but different filetypes - -Table Editor ------------- - - use PSI Tree as data source - integrate with native IntelliJ IDE document change handler (e.g. for undo/redo) - simplify UI/UX & remove header toolbar @@ -210,22 +190,6 @@ Table Editor ### Changed -- support comments in fast lexer - -### Changed - -- reworked (rainbow) coloring - -### Changed - -- avoid formatting while typing - -### Changed - -- limit column highlighting to 1000 entries around caret - -### Changed - - limit calculation and buffering of CSV column info data ### Fixed @@ -237,7 +201,6 @@ Table Editor ### Fixed - Cannot load from object array because "data" is null #335 #337 - - Empty comment indicator ### Added @@ -293,6 +256,7 @@ NOTE: Minimum version requirement changed to v2020.1 and newer ### Added - Plugin name ### Changed + - CSV ### Fixed @@ -387,11 +351,6 @@ NOTE: Minimum version requirement changed to v2020.1 and newer ### Added -- Predefined column colors (Rainbow-style) -- Enhanced color scheme switch - -### Added - - Table Editor coloring ## 2.13.0 - Jul 20, 2020 @@ -457,11 +416,6 @@ NOTE: Minimum version requirement changed to v2020.1 and newer ### Added -- customizable escape character #159 -- value separator setting moved from 'Code Style' to 'General' - -### Added - - lots of cleanup & rework ## 2.8.2 - Jan 22, 2020 @@ -553,11 +507,6 @@ Support for IDE v192.* ### Added -- option to keep/ignore a linebreak at the end of a file (table editor) -- improved change detection of table editor to avoid overwriting original text representation without editing any values - -### Added - - file based value separator (e.g. ',' or ';') ## 2.3.1 - Mar 31, 2019 @@ -604,11 +553,6 @@ Support for IDE v192.* ### Added -- support column highlighting for table editor -- support all kind of text attributes for column highlighting - -### Added - - table editor values not longer enforced to be quoted on save (customizable) ### Fixed @@ -645,11 +589,6 @@ Support for IDE v192.* ### Added -- CSV/TSV editor settings (File > Settings > General > CSV/TSV Editor) -- TAB (separator) highlighting - -### Added - - Enable/disable balloon info - Soft wrap settings specific for CSV/TSV @@ -706,16 +645,6 @@ Support for IDE v192.* ### Added -- TSV file support -TSV files a recognized as such but treated as a variant of CSV files, the same syntax highlighting and code style settings are applied. - -### Added - -- tab (↹) and pipe (|) as separators added -- spellchecker enabled - -### Added - - it was necessary to increase the minimum IDE version from 2016.1.1 to 2016.3.2 due to a required fix in the formatting code. Previous versions of the plugin can still be downloaded directly from Jetbrains Plugin Repository. ## 1.5.1 - Mar 21, 2018 diff --git a/gradle.properties b/gradle.properties index fbef94e..46a904f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ pluginName=CSV Editor pluginId=net.seesharpsoft.intellij.plugins.csv -pluginVersion=4.1.0 +pluginVersion=4.2.0 pluginSinceBuild=242 diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvGithubIssueSubmitter.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvGithubIssueSubmitter.java index 8e40ec8..7dbcad4 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvGithubIssueSubmitter.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvGithubIssueSubmitter.java @@ -39,6 +39,7 @@ public class CsvGithubIssueSubmitter extends ErrorReportSubmitter { public static final String GIT_USER = "SeeSharpSoft"; public static final String GIT_REPO = "intellij-csv-validator"; public static final GHRepositoryPath GITHUB_FULL_PATH = new GHRepositoryPath(GIT_USER, GIT_REPO); + private static final String REPORT_ACTION_TEXT = "Report to 'CSV Editor' (Github)"; private static ScheduledFuture recentlySentReport = null; private static final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor(); @@ -52,7 +53,7 @@ private static class CsvGithubSubmitException extends RuntimeException { @NotNull @Override public String getReportActionText() { - return "Report to 'CSV Editor' (Github)"; + return REPORT_ACTION_TEXT; } @Override @@ -94,7 +95,7 @@ protected boolean submit(IdeaLoggingEvent event, String additionalInfo, Project GithubApiRequestExecutor githubExecutor = GithubApiRequestExecutor.Factory.getInstance().create(account.getServer(), token); - Task submitTask = new Task.Backgroundable(project, getReportActionText()) { + Task submitTask = new Task.Backgroundable(project, REPORT_ACTION_TEXT) { @Override public void run(@NotNull ProgressIndicator indicator) { submitToGithub(event, additionalInfo, githubExecutor, consumer, indicator); @@ -160,7 +161,15 @@ protected GithubApiRequest createNewIssue(String title, String content) throw } protected String searchExistingIssues(GithubApiRequestExecutor githubExecutor, String title, ProgressIndicator progressIndicator) throws IOException { + // Create a search needle from the title but ensure it is never null/empty to avoid GitHub 422 (Validation Failed) String needle = title.replaceAll("\\s*(\\[.*?]|\\(.*?\\)|\\{.*?})\\s*", ""); + + // If the sanitized title becomes empty (e.g., only brackets present), fall back to the raw title + if (Strings.isEmptyOrSpaces(needle)) { + needle = title == null ? "" : title.trim(); + } + + // Apply length cap with word boundary if possible if (needle.length() > 250) { int endIndex = needle.substring(0, 250).lastIndexOf(" "); if (endIndex == -1) { @@ -168,6 +177,11 @@ protected String searchExistingIssues(GithubApiRequestExecutor githubExecutor, S } needle = needle.substring(0, endIndex); } + + // Final safety: if still empty, use a minimal non-empty token to satisfy GitHub API requirements + if (Strings.isEmptyOrSpaces(needle)) { + needle = "crash"; + } GithubApiRequest> existingIssueRequest = GithubApiRequests.Search.Issues.get( GithubServerPath.DEFAULT_SERVER, 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 ec75dd2..4771717 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvParserDefinition.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvParserDefinition.java @@ -55,14 +55,13 @@ public PsiParser createParser(final Project project) { } @Override - public IFileElementType getFileNodeType() { + public @NotNull IFileElementType getFileNodeType() { return FILE; } @Override - public PsiFile createFile(FileViewProvider viewProvider) { + public @NotNull PsiFile createFile(@NotNull FileViewProvider viewProvider) { return new CsvFile(viewProvider, viewProvider.getFileType()); -// return new CsvFile(viewProvider, CsvFileType.INSTANCE); } @Override diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvPlugin.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvPlugin.java index 4810f92..ad399bc 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvPlugin.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/CsvPlugin.java @@ -1,8 +1,9 @@ package net.seesharpsoft.intellij.plugins.csv; import com.intellij.ide.BrowserUtil; -import com.intellij.ide.actions.ShowSettingsUtilImpl; import com.intellij.notification.*; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.options.ShowSettingsUtil; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; @@ -22,11 +23,14 @@ public class CsvPlugin implements ProjectActivity, DumbAware { private static void openLink(Project project, String link) { if (project.isDisposed()) return; - if (link.startsWith("#")) { - ShowSettingsUtilImpl.showSettingsDialog(project, link.substring(1), null); - } else { - BrowserUtil.browse(link, project); - } + ApplicationManager.getApplication().invokeLater(() -> + { + if (link.startsWith("#")) { + ShowSettingsUtil.getInstance().showSettingsDialog(project, link.substring(1)); + } else { + BrowserUtil.browse(link, project); + } + }); } public static void doAsyncProjectMaintenance(@NotNull Project project) { @@ -42,7 +46,7 @@ public void run(@NotNull ProgressIndicator progressIndicator) { // start process try { CsvFileAttributes csvFileAttributes = CsvFileAttributes.getInstance(getProject()); - csvFileAttributes.cleanupAttributeMap(getProject()); + csvFileAttributes.cleanupAttributeMap(project); } catch (Exception exception) { // repeated unresolved bug-reports when retrieving the component // while this cleanup is an optional and non-critical task @@ -75,10 +79,10 @@ public void run(@NotNull ProgressIndicator progressIndicator) { NotificationType.INFORMATION ); - notification.addAction(NotificationAction.create("General Settings", (anActionEvent, notification1) -> { + notification.addAction(NotificationAction.create("General settings", (anActionEvent, notification1) -> { openLink(project, "#" + CsvEditorSettingsProvider.CSV_EDITOR_SETTINGS_ID); })); - notification.addAction(NotificationAction.create("Color Scheme", (anActionEvent, notification1) -> { + notification.addAction(NotificationAction.create("Color scheme", (anActionEvent, notification1) -> { openLink(project, "#reference.settingsdialog.IDE.editor.colors.CSV/TSV/PSV"); })); notification.addAction(NotificationAction.create("Formatting", (anActionEvent, notification1) -> { diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/CsvAnnotator.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/CsvAnnotator.java index 3df77ba..1e727f4 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/CsvAnnotator.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/CsvAnnotator.java @@ -56,14 +56,10 @@ public void annotate(@NotNull final PsiElement element, @NotNull final Annotatio ) ); - AnnotationBuilder annotationBuilder = holder.newAnnotation(CSV_COLUMN_INFO_SEVERITY, message) + holder.newAnnotation(CSV_COLUMN_INFO_SEVERITY, message) .range(element) - .needsUpdateOnTyping(false); - - if (tooltip != null) { - annotationBuilder.tooltip(tooltip); - } - annotationBuilder.create(); + .tooltip(tooltip) + .create(); } } @@ -98,7 +94,6 @@ protected boolean handleSeparatorElement(@NotNull PsiElement element, @NotNull A holder.newAnnotation(CSV_COLUMN_INFO_SEVERITY, "↹") .range(element) .enforcedTextAttributes(textAttributes) - .needsUpdateOnTyping(false) .create(); } return true; diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/CsvFileEditorProvider.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/CsvFileEditorProvider.java index cbace8b..c9f1cc2 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/CsvFileEditorProvider.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/CsvFileEditorProvider.java @@ -48,7 +48,19 @@ public static boolean acceptCsvFile(@NotNull Project project, @NotNull VirtualFi @Override public boolean accept(@NotNull Project project, @NotNull VirtualFile file) { - return CsvFileEditorProvider.acceptCsvFile(project, file); + // Guard against cases where the platform TextEditor can't be created for the given file + // (e.g., special virtual files used by Structure View or diff). In such cases, delegating + // to TextEditorProvider would lead to NPEs inside platform code. + if (!CsvFileEditorProvider.acceptCsvFile(project, file)) { + return false; + } + try { + TextEditorProvider textEditorProvider = TextEditorProvider.getInstance(); + return textEditorProvider != null && textEditorProvider.accept(project, file); + } catch (Throwable t) { + // Be conservative on any unexpected error and do not accept the file to avoid IDE crashes. + return false; + } } protected void applySettings(EditorSettings editorSettings, CsvEditorSettings csvEditorSettings) { @@ -87,6 +99,10 @@ public Builder createEditorAsync(@NotNull Project project, @NotNull VirtualFile @Override public @NotNull FileEditor build() { TextEditorProvider provider = TextEditorProvider.getInstance(); + // Safety check: ensure provider accepts the file before trying to create the editor + if (provider == null || !provider.accept(project, virtualFile)) { + throw new IllegalStateException("TextEditorProvider does not accept file: " + virtualFile); + } TextEditor textEditor = (TextEditor) provider.createEditor(project, virtualFile); applySettings(textEditor.getEditor().getSettings(), CsvEditorSettings.getInstance()); return textEditor; 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 a5b5dc0..9faad37 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 @@ -213,14 +213,34 @@ public final CsvFile getCsvFile() { if (project.isDisposed()) { return null; } + + // ensure we have a document reference + if (this.document == null) { + this.document = FileDocumentManager.getInstance().getDocument(this.file); + } + if (this.psiFile == null || !this.psiFile.isValid()) { - this.psiFile = ReadAction.compute(() -> { - this.document = FileDocumentManager.getInstance().getDocument(this.file); - PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project); - return documentManager.getPsiFile(this.document); - }); - this.currentSeparator = CsvHelper.getValueSeparator(this.psiFile); - this.currentEscapeCharacter = CsvHelper.getEscapeCharacter(this.psiFile); + // On EDT, avoid potentially slow PSI/index operations – use cached PSI only + if (ApplicationManager.getApplication().isDispatchThread()) { + if (this.document != null) { + this.psiFile = PsiDocumentManager.getInstance(project).getCachedPsiFile(this.document); + } + } else { + // Off EDT it is safe to resolve PSI + this.psiFile = ReadAction.compute(() -> { + if (this.document == null) { + this.document = FileDocumentManager.getInstance().getDocument(this.file); + } + if (this.document == null) return null; + PsiDocumentManager documentManager = PsiDocumentManager.getInstance(project); + return documentManager.getPsiFile(this.document); + }); + } + + if (this.psiFile != null) { + this.currentSeparator = CsvHelper.getValueSeparator(this.psiFile); + this.currentEscapeCharacter = CsvHelper.getEscapeCharacter(this.psiFile); + } } return this.psiFile instanceof CsvFile ? (CsvFile) psiFile : null; } diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/CsvTableModelBase.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/CsvTableModelBase.java index 65b7f85..3ef2476 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/CsvTableModelBase.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/CsvTableModelBase.java @@ -1,5 +1,8 @@ package net.seesharpsoft.intellij.plugins.csv.editor.table; +import com.intellij.openapi.application.ModalityState; +import com.intellij.openapi.application.ReadAction; +import com.intellij.util.concurrency.AppExecutorUtil; import com.intellij.psi.*; import com.intellij.psi.util.PsiTreeUtil; import net.seesharpsoft.intellij.plugins.csv.components.CsvEscapeCharacter; @@ -46,25 +49,29 @@ public T getPsiFileHolder() { protected void addPsiTreeChangeListener() { PsiFile psiFile = getPsiFile(); - if (psiFile == null) return; + if (psiFile == null) { + // Resolve PSI off-EDT and register the listener when available to avoid slow operations on EDT + ReadAction + .nonBlocking(this::getPsiFile) + .coalesceBy(this) + .finishOnUiThread(ModalityState.any(), pf -> { + if (pf == null) return; + PsiManager mgr = pf.getManager(); + if (mgr == null) return; + mgr.addPsiTreeChangeListener(myPsiTreeChangeListener, myPsiFileHolder); + }) + .submit(AppExecutorUtil.getAppExecutorService()); + return; + } PsiManager manager = psiFile.getManager(); if (manager == null) return; manager.addPsiTreeChangeListener(myPsiTreeChangeListener, myPsiFileHolder); } - protected void removePsiTreeChangeListener() { - PsiFile psiFile = getPsiFile(); - if (psiFile == null) return; - PsiManager manager = psiFile.getManager(); - if (manager == null) return; - manager.removePsiTreeChangeListener(myPsiTreeChangeListener); - } - @Override public void dispose() { CsvTableModel.super.dispose(); myPsiTreeUpdater.dispose(); - removePsiTreeChangeListener(); } private void onPsiTreeChanged(@Nullable PsiFile file) { diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/swing/CsvMultiLineCellRenderer.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/swing/CsvMultiLineCellRenderer.java index 74517e7..cdc5dec 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/swing/CsvMultiLineCellRenderer.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/swing/CsvMultiLineCellRenderer.java @@ -35,6 +35,7 @@ public class CsvMultiLineCellRenderer extends JBScrollPane implements TableCellR public CsvMultiLineCellRenderer(CsvTableEditorKeyListener keyListener, UserDataHolder userDataHolderParam) { this.myUserDataHolder = userDataHolderParam; myTextArea = new JTextArea(); + // Keep wrapping enabled for the editor use case. Renderers will no longer use this class. myTextArea.setLineWrap(true); myTextArea.setWrapStyleWord(true); myTextArea.setOpaque(true); @@ -88,10 +89,9 @@ public Component getTableCellRendererComponent(JTable table, Object value, boole final int columnWidth = table.getColumnModel().getColumn(column).getWidth(); final int rowHeight = table.getRowHeight(row); + // Apply sizes but avoid triggering validation/layout here to prevent heavy layout work during painting this.setSize(columnWidth, rowHeight); - this.validate(); myTextArea.setSize(columnWidth, rowHeight); - myTextArea.validate(); return this; } diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/swing/CsvTable.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/swing/CsvTable.java index d3e5424..cc53bd4 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/swing/CsvTable.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/editor/table/swing/CsvTable.java @@ -185,7 +185,7 @@ private void paintCell(Graphics g, Rectangle cellRect, int row, int column) { table.getEditingColumn() == column) { Component component = table.getEditorComponent(); component.setBounds(cellRect); - component.validate(); + // Avoid triggering validation/layout during painting to prevent EDT freezes } else { TableCellRenderer renderer = table.getCellRenderer(row, column); Component component = table.prepareRenderer(renderer, row, column); diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/highlighter/CsvHighlightingElement.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/highlighter/CsvHighlightingElement.java index a3bc3f2..1c3dc7b 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/highlighter/CsvHighlightingElement.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/highlighter/CsvHighlightingElement.java @@ -1,9 +1,9 @@ package net.seesharpsoft.intellij.plugins.csv.highlighter; import com.intellij.openapi.editor.HighlighterColors; +import com.intellij.lang.Language; import com.intellij.openapi.editor.colors.TextAttributesKey; import com.intellij.psi.tree.IElementType; -import net.seesharpsoft.intellij.plugins.csv.CsvLanguage; import net.seesharpsoft.intellij.plugins.csv.psi.CsvTypes; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; @@ -21,7 +21,8 @@ class TokenBased extends IElementType implements CsvHighlightingElement { private final TextAttributesKey[] myTextAttributesKeys; private TokenBased(@NonNls @NotNull String debugName, TextAttributesKey textAttributesKey) { - super(debugName, CsvLanguage.INSTANCE); + // Use a neutral language to avoid element type id issues across IDE versions + super(debugName, Language.ANY); myTextAttributesKeys = new TextAttributesKey[]{textAttributesKey}; } diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/inspection/CsvValidationInspection.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/inspection/CsvValidationInspection.java index cc9dd57..446ba6d 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/inspection/CsvValidationInspection.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/inspection/CsvValidationInspection.java @@ -60,7 +60,7 @@ public String getGroupDisplayName() { @NotNull public String getShortName() { - return "CsvValidation"; + return "net.seesharpsoft.intellij.plugins.csv.inspection.CsvValidationInspection"; } @Override diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/psi/CsvFile.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/psi/CsvFile.java index 2d68833..eadcc71 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/psi/CsvFile.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/psi/CsvFile.java @@ -37,6 +37,27 @@ public Icon getIcon(int flags) { @Override public PsiReference @NotNull [] getReferences() { + // Defensive: avoid creating references for invalid/non-physical files. + // Certain IDE operations (e.g., Safe Delete) may attempt to create SmartPointers + // for file-level references. If the underlying VirtualFile has been invalidated + // (e.g., file deleted/moved or non-physical), creating such pointers can throw + // PluginException: PsiUtilCore.ensureValid(...). To prevent that, short-circuit + // and return no references when this PsiFile isn't in a safe/valid state. + if (!isValid()) { + return PsiReference.EMPTY_ARRAY; + } + + // Check associated virtual file state as additional safeguard + var virtualFile = getVirtualFile(); + if (virtualFile == null || !virtualFile.isValid()) { + return PsiReference.EMPTY_ARRAY; + } + + // Skip non-physical files (light or in-memory) to avoid unexpected pointer creation + if (!getViewProvider().isPhysical()) { + return PsiReference.EMPTY_ARRAY; + } + return ReferenceProvidersRegistry.getReferencesFromProviders(this); } } \ No newline at end of file diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/settings/CsvCodeStyleSettingsProvider.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/settings/CsvCodeStyleSettingsProvider.java index 41b56da..23175f0 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/settings/CsvCodeStyleSettingsProvider.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/settings/CsvCodeStyleSettingsProvider.java @@ -94,8 +94,27 @@ public LanguageCodeStyleSettingsProvider.SettingsType getSettingsType() { @Override protected @NotNull PsiFile doReformat(Project project, @NotNull PsiFile psiFile) { - CodeStyleManager.getInstance(project).reformatText(psiFile, 0, psiFile.getTextLength()); - return psiFile; + // Defensive: PSI might be invalidated by the time the settings UI triggers reformat. + // Guard against invalid files and run inside a read action to avoid race conditions. + try { + return com.intellij.openapi.application.ReadAction.compute(() -> { + if (!psiFile.isValid()) { + return psiFile; + } + int endOffset; + try { + endOffset = psiFile.getTextLength(); + } catch (Throwable t) { + // If accessing text length fails due to invalidation, skip reformat. + return psiFile; + } + CodeStyleManager.getInstance(project).reformatText(psiFile, 0, endOffset); + return psiFile; + }); + } catch (Throwable ignored) { + // As a last resort, do nothing to avoid PluginException surfacing to users. + return psiFile; + } } } } diff --git a/src/main/java/net/seesharpsoft/intellij/plugins/csv/settings/CsvEditorSettings.java b/src/main/java/net/seesharpsoft/intellij/plugins/csv/settings/CsvEditorSettings.java index 8ca09f1..f42b302 100644 --- a/src/main/java/net/seesharpsoft/intellij/plugins/csv/settings/CsvEditorSettings.java +++ b/src/main/java/net/seesharpsoft/intellij/plugins/csv/settings/CsvEditorSettings.java @@ -195,7 +195,13 @@ public void setTabHighlightColor(Color color) { } public EditorPrio getEditorPrio() { - return getState().EDITOR_PRIO; + // Important: avoid triggering OptionSet.init() here because it consults + // EditorSettingsExternalizable on first access which may require UI/EDT + // initialization. The file editor providers call this method from background + // threads during provider discovery, and any slow or blocking initialization + // can lead to timeouts when the IDE checks providers. + // Access the current option directly to keep provider checks fast and non-blocking. + return this.myOptions.EDITOR_PRIO; } public void setEditorPrio(EditorPrio editorPrio) { diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 35de739..7e2f544 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -98,7 +98,7 @@ language="csv" enabledByDefault="true" groupName="CSV" - shortName="CsvValidation" + shortName="net.seesharpsoft.intellij.plugins.csv.inspection.CsvValidationInspection" implementationClass="net.seesharpsoft.intellij.plugins.csv.inspection.CsvValidationInspection"/> diff --git a/src/test/java/net/seesharpsoft/intellij/plugins/csv/CsvParserDefinitionIntegrationTest.java b/src/test/java/net/seesharpsoft/intellij/plugins/csv/CsvParserDefinitionIntegrationTest.java new file mode 100644 index 0000000..83f5bf1 --- /dev/null +++ b/src/test/java/net/seesharpsoft/intellij/plugins/csv/CsvParserDefinitionIntegrationTest.java @@ -0,0 +1,57 @@ +package net.seesharpsoft.intellij.plugins.csv; + +import com.intellij.lang.LanguageParserDefinitions; +import com.intellij.lexer.Lexer; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiFile; +import net.seesharpsoft.intellij.plugins.csv.psi.CsvFile; +import org.jetbrains.annotations.NotNull; + +public class CsvParserDefinitionIntegrationTest extends CsvBasePlatformTestCase { + + public void testPsiFileUsesProviderFileTypeForCsv() { + PsiFile psiFile = myFixture.addFileToProject("parser/TypeCheck.csv", "a,b\nc,d"); + assertInstanceOf(psiFile, CsvFile.class); + VirtualFile vFile = psiFile.getVirtualFile(); + assertNotNull(vFile); + FileType vfType = vFile.getFileType(); + assertEquals("csv", vfType.getDefaultExtension()); + assertSame(vfType, psiFile.getFileType()); + } + + public void testPsiFileUsesProviderFileTypeForTsv() { + PsiFile psiFile = myFixture.addFileToProject("parser/TypeCheck.tsv", "a\tb\n\tc"); + assertInstanceOf(psiFile, CsvFile.class); + VirtualFile vFile = psiFile.getVirtualFile(); + assertNotNull(vFile); + FileType vfType = vFile.getFileType(); + assertEquals("tsv", vfType.getDefaultExtension()); + assertSame(vfType, psiFile.getFileType()); + } + + public void testCreateLexerOverloads() { + CsvParserDefinition def = (CsvParserDefinition) LanguageParserDefinitions.INSTANCE.forLanguage(CsvLanguage.INSTANCE); + assertNotNull(def); + + PsiFile psiFile = myFixture.addFileToProject("parser/Lexer.csv", "1,2,3"); + assertInstanceOf(psiFile, CsvFile.class); + + // The PsiFile overload should work and provide a lexer + Lexer lexer = def.createLexer(psiFile); + assertNotNull(lexer); + + // The Project overload is intentionally unsupported and should throw + boolean threw = false; + try { + def.createLexer(getProject()); + } catch (UnsupportedOperationException expected) { + threw = true; + } + assertTrue("Expected UnsupportedOperationException for createLexer(Project)", threw); + } + + private static @NotNull CsvParserDefinition getCsvParserDefinition() { + return (CsvParserDefinition) LanguageParserDefinitions.INSTANCE.forLanguage(CsvLanguage.INSTANCE); + } +} diff --git a/src/test/java/net/seesharpsoft/intellij/plugins/csv/psi/CsvFileReferencesDefensiveTest.java b/src/test/java/net/seesharpsoft/intellij/plugins/csv/psi/CsvFileReferencesDefensiveTest.java new file mode 100644 index 0000000..2867dd3 --- /dev/null +++ b/src/test/java/net/seesharpsoft/intellij/plugins/csv/psi/CsvFileReferencesDefensiveTest.java @@ -0,0 +1,50 @@ +package net.seesharpsoft.intellij.plugins.csv.psi; + +import com.intellij.openapi.application.WriteAction; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiManager; +import com.intellij.psi.PsiReference; +import com.intellij.testFramework.LightVirtualFile; +import net.seesharpsoft.intellij.plugins.csv.CsvBasePlatformTestCase; +import net.seesharpsoft.intellij.plugins.csv.CsvFileType; + +public class CsvFileReferencesDefensiveTest extends CsvBasePlatformTestCase { + + public void testNonPhysicalCsvFileReturnsNoReferences() { + // Use a LightVirtualFile to guarantee a non-physical provider across platform versions + LightVirtualFile vFile = new LightVirtualFile("NonPhysical.csv", CsvFileType.INSTANCE, "a,b\nc,d"); + PsiFile psiFile = PsiManager.getInstance(getProject()).findFile(vFile); + assertNotNull(psiFile); + assertInstanceOf(psiFile, CsvFile.class); + // Ensure this is a non-physical file (LightVirtualFile-backed) + assertFalse(psiFile.getViewProvider().isPhysical()); + + PsiReference[] refs = psiFile.getReferences(); + assertNotNull(refs); + assertEquals(0, refs.length); + } + + public void testInvalidatedCsvFileReturnsNoReferences() throws Exception { + PsiFile psiFile = myFixture.addFileToProject("refs/physical.csv", "x,y\n1,2"); + assertInstanceOf(psiFile, CsvFile.class); + VirtualFile vFile = psiFile.getVirtualFile(); + assertNotNull(vFile); + + // Invalidate file by deleting the underlying VirtualFile + WriteAction.run(() -> { + try { + vFile.delete(this); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + + assertFalse("PsiFile should be invalid after deleting VirtualFile", psiFile.isValid()); + + // Must not throw, and must return empty references + PsiReference[] refs = psiFile.getReferences(); + assertNotNull(refs); + assertEquals(0, refs.length); + } +}