diff --git a/java/java-impl/src/com/intellij/ide/actions/JavaScratchFileCreationHelper.java b/java/java-impl/src/com/intellij/ide/actions/JavaScratchFileCreationHelper.java new file mode 100644 index 000000000000..8538bd90c583 --- /dev/null +++ b/java/java-impl/src/com/intellij/ide/actions/JavaScratchFileCreationHelper.java @@ -0,0 +1,60 @@ +/* + * Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ +package com.intellij.ide.actions; + +import com.intellij.ide.scratch.ScratchFileCreationHelper; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.*; +import org.jetbrains.annotations.NotNull; + +/** + * @author gregsh + */ +public class JavaScratchFileCreationHelper extends ScratchFileCreationHelper { + + @Override + public boolean prepareText(@NotNull Project project, @NotNull Context context, @NotNull DataContext dataContext) { + String caretMarker = "CARET_MARKER"; + if (context.text == "") { + String text = reformat( + project, context.language, + "class Scratch { public static void main (String[] args) {\n" + caretMarker + "\n} }"); + context.caretOffset = text.indexOf(caretMarker); + context.text = text.substring(0, context.caretOffset) + text.substring(context.caretOffset + caretMarker.length()); + return true; + } + //todo add required import statements from dataContext if available + PsiFile psi = parseHeader(project, context.language, context.text); + SyntaxTraverser s = SyntaxTraverser.psiTraverser(); + if (s.withRoot(psi).traverse().filter(PsiClass.class).first() != null || + s.withRoot(psi).traverse().filter(PsiErrorElement.class).first() == null) { + return true; + } + psi = parseHeader(project, context.language, "class Scratch {\n" + context.text + "\n}"); + if (s.withRoot(psi).traverse().filter(PsiMethod.class).first() != null) { + String text = reformat( + project, context.language, + "class Scratch {\n" + caretMarker + "\n}"); + context.caretOffset = text.indexOf(caretMarker); + context.text = text.substring(0, context.caretOffset) + + StringUtil.trim(context.text) + + text.substring(context.caretOffset + caretMarker.length()); + return true; + } + + psi = parseHeader(project, context.language, "class Scratch {\n" + context.text + "\n}"); + if (s.withRoot(psi).traverse().filter(PsiMember.class).first() != null) { + String text = reformat(project, context.language, + "class Scratch { public static void main (String[] args) {\n" + caretMarker + "\n} }"); + context.caretOffset = text.indexOf(caretMarker); + context.text = text.substring(0, context.caretOffset) + + StringUtil.trim(context.text) + + text.substring(context.caretOffset + caretMarker.length()); + return true; + } + return false; + } +} diff --git a/platform/lang-impl/src/com/intellij/ide/scratch/ScratchFileActions.java b/platform/lang-impl/src/com/intellij/ide/scratch/ScratchFileActions.java index 5b970fe67e19..e871fa71a53b 100644 --- a/platform/lang-impl/src/com/intellij/ide/scratch/ScratchFileActions.java +++ b/platform/lang-impl/src/com/intellij/ide/scratch/ScratchFileActions.java @@ -26,8 +26,8 @@ import com.intellij.lang.injection.InjectedLanguageManager; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.editor.Caret; import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.fileEditor.FileEditorManager; -import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.fileEditor.OpenFileDescriptor; +import com.intellij.openapi.fileTypes.InternalFileType; import com.intellij.openapi.fileTypes.LanguageFileType; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; @@ -35,7 +35,10 @@ import com.intellij.openapi.util.Condition; import com.intellij.openapi.util.registry.Registry; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vfs.VirtualFile; -import com.intellij.psi.*; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiErrorElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.SyntaxTraverser; import com.intellij.ui.LayeredIcon; import com.intellij.util.Consumer; import com.intellij.util.Function; @@ -57,6 +60,14 @@ import static com.intellij.openapi.util.Conditions.notNull; */ public class ScratchFileActions { + private static int ourCurrentBuffer = 0; + + private static int nextBufferIndex() { + ourCurrentBuffer = (ourCurrentBuffer % Registry.intValue("ide.scratch.buffers")) + 1; + return ourCurrentBuffer; + } + + public static class NewFileAction extends DumbAwareAction { private static final Icon ICON = LayeredIcon.create(AllIcons.FileTypes.Text, AllIcons.Actions.Scratch); @@ -67,8 +78,7 @@ public class ScratchFileActions { private final String myActionText; public NewFileAction() { - final Presentation templatePresentation = getTemplatePresentation(); - templatePresentation.setIcon(ICON); + getTemplatePresentation().setIcon(ICON); // A hacky way for customizing text in IDEs without File->New-> submenu myActionText = (isIdeWithoutNewSubmenu() ? "New " : "") + ActionsBundle.actionText(ACTION_ID); } @@ -88,21 +98,18 @@ public class ScratchFileActions { @Override public void actionPerformed(@NotNull AnActionEvent e) { - final Project project = e.getProject(); + Project project = e.getProject(); if (project == null) return; - PsiFile file = e.getData(CommonDataKeys.PSI_FILE); - Editor editor = e.getData(CommonDataKeys.EDITOR); - - String eventText = getSelectionText(editor); - if (eventText == null) { - eventText = e.getData(PlatformDataKeys.PREDEFINED_TEXT); - } - final String text = StringUtil.notNullize(eventText); - Language language = text.isEmpty() ? null : detectLanguageFromSelection(project, editor, file, text); - Consumer consumer = language1 -> doCreateNewScratch(project, false, language1, text); - if (language != null) { - consumer.consume(language); + ScratchFileCreationHelper.Context context = createContext(project, e); + Consumer consumer = l -> { + context.language = l; + ScratchFileCreationHelper.EXTENSION.forLanguage(context.language).prepareText( + project, context, DataContext.EMPTY_CONTEXT); + doCreateNewScratch(project, context); + }; + if (context.language != null) { + consumer.consume(context.language); } else { LRUPopupBuilder.forFileLanguages(project, null, consumer).showCenteredInCurrentWindow(project); @@ -126,20 +133,6 @@ public class ScratchFileActions { } } - @Nullable - private static Language detectLanguageFromSelection(Project project, Editor editor, PsiFile file, String text) { - Language language = getLanguageFromCaret(project, editor, file); - FileType fileType = LanguageUtil.getLanguageFileType(language); - if (fileType == null) return null; - - CharSequence fileSnippet = text.subSequence(0, Math.min(text.length(), 10 * 1024)); - PsiFileFactory fileFactory = PsiFileFactory.getInstance(file.getProject()); - PsiFile psiFile = fileFactory.createFileFromText("a." + fileType.getDefaultExtension(), language, fileSnippet); - PsiErrorElement firstError = SyntaxTraverser.psiTraverser(psiFile).traverse().filter(PsiErrorElement.class).first(); - // heuristics: first error must not be right under the file PSI - return firstError == null || firstError.getParent() != psiFile ? language : null; - } - public static class NewBufferAction extends DumbAwareAction { @Override @@ -152,34 +145,65 @@ public class ScratchFileActions { public void actionPerformed(@NotNull AnActionEvent e) { Project project = e.getProject(); if (project == null) return; - - PsiFile file = e.getData(CommonDataKeys.PSI_FILE); - Editor editor = e.getData(CommonDataKeys.EDITOR); - - String text = StringUtil.notNullize(getSelectionText(editor)); - Language language = text.isEmpty() ? null : detectLanguageFromSelection(project, editor, file, text); - - doCreateNewScratch(project, true, ObjectUtils.notNull(language, StdLanguages.TEXT), text); + ScratchFileCreationHelper.Context context = createContext(project, e); + context.filePrefix = "buffer"; + context.createOption = ScratchFileService.Option.create_if_missing; + context.fileCounter = ScratchFileActions::nextBufferIndex; + if (context.language == null) context.language = StdLanguages.TEXT; + doCreateNewScratch(project, context); } } - static void doCreateNewScratch(@NotNull Project project, boolean buffer, @NotNull Language language, @NotNull String text) { + @NotNull + static ScratchFileCreationHelper.Context createContext(@NotNull Project project, @NotNull AnActionEvent e) { + PsiFile file = e.getData(CommonDataKeys.PSI_FILE); + Editor editor = e.getData(CommonDataKeys.EDITOR); + + ScratchFileCreationHelper.Context context = new ScratchFileCreationHelper.Context(); + context.text = StringUtil.notNullize(getSelectionText(editor)); + if (!context.text.isEmpty()) { + context.language = getLanguageFromCaret(project, editor, file); + checkLanguageAndTryToFixText(project, context, e.getDataContext()); + } + else { + context.text = StringUtil.notNullize(e.getData(PlatformDataKeys.PREDEFINED_TEXT)); + } + return context; + } + + static void doCreateNewScratch(@NotNull Project project, @NotNull ScratchFileCreationHelper.Context context) { FeatureUsageTracker.getInstance().triggerFeatureUsed("scratch"); + Language language = ObjectUtils.notNull(context.language); + if (context.fileExtension == null) { + LanguageFileType fileType = language.getAssociatedFileType(); + context.fileExtension = fileType == null ? "" : fileType.getDefaultExtension(); + } + ScratchFileCreationHelper.EXTENSION.forLanguage(language).beforeCreate(project, context); - LanguageFileType fileType = language.getAssociatedFileType(); - String ext = buffer || fileType == null? "" : fileType.getDefaultExtension(); - String fileName = buffer ? "buffer" + nextBufferIndex() : "scratch"; - ScratchFileService.Option option = buffer ? ScratchFileService.Option.create_if_missing : ScratchFileService.Option.create_new_always; - VirtualFile f = ScratchRootType.getInstance().createScratchFile(project, PathUtil.makeFileName(fileName, ext), language, text, option); + String fileName = PathUtil.makeFileName(ObjectUtils.notNull(context.filePrefix, "scratch") + + (context.fileCounter != null ? context.fileCounter.create() : ""), + context.fileExtension); + VirtualFile f = ScratchRootType.getInstance().createScratchFile( + project, fileName, language, context.text, context.createOption); if (f != null) { - FileEditorManager.getInstance(project).openFile(f, true); + new OpenFileDescriptor(project, f, context.caretOffset).navigate(true); } } - private static int ourCurrentBuffer = 0; - private static int nextBufferIndex() { - ourCurrentBuffer = (ourCurrentBuffer % Registry.intValue("ide.scratch.buffers")) + 1; - return ourCurrentBuffer; + private static void checkLanguageAndTryToFixText(@NotNull Project project, + @NotNull ScratchFileCreationHelper.Context context, + @NotNull DataContext dataContext) { + if (context.language == null) return; + ScratchFileCreationHelper handler = ScratchFileCreationHelper.EXTENSION.forLanguage(context.language); + if (handler.prepareText(project, context, dataContext)) return; + + PsiFile psiFile = ScratchFileCreationHelper.parseHeader(project, context.language, context.text); + PsiErrorElement firstError = SyntaxTraverser.psiTraverser(psiFile).traverse().filter(PsiErrorElement.class).first(); + // heuristics: first error must not be right under the file PSI + // otherwise let the user choose the language manually + if (firstError != null && firstError.getParent() == psiFile) { + context.language = null; + } } @Nullable @@ -197,7 +221,11 @@ public class ScratchFileActions { int offset = caret.getOffset(); PsiElement element = InjectedLanguageManager.getInstance(project).findInjectedElementAt(psiFile, offset); PsiFile file = element != null ? element.getContainingFile() : psiFile; - return file.getLanguage(); + Language language = file.getLanguage(); + if (language == StdLanguages.TEXT && file.getFileType() instanceof InternalFileType) { + return StdLanguages.XML; + } + return language; } public static class LanguageAction extends DumbAwareAction { diff --git a/platform/lang-impl/src/com/intellij/ide/scratch/ScratchFileCreationHelper.java b/platform/lang-impl/src/com/intellij/ide/scratch/ScratchFileCreationHelper.java new file mode 100644 index 000000000000..aa75126127fd --- /dev/null +++ b/platform/lang-impl/src/com/intellij/ide/scratch/ScratchFileCreationHelper.java @@ -0,0 +1,73 @@ +/* + * Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. + */ +package com.intellij.ide.scratch; + +import com.intellij.lang.Language; +import com.intellij.lang.LanguageExtension; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.command.WriteCommandAction; +import com.intellij.openapi.fileTypes.LanguageFileType; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Computable; +import com.intellij.openapi.util.Factory; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiFileFactory; +import com.intellij.psi.codeStyle.CodeStyleManager; +import com.intellij.util.PathUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author gregsh + */ +public abstract class ScratchFileCreationHelper { + public static final LanguageExtension EXTENSION = new LanguageExtension<>( + "com.intellij.scratch.creationHelper", new ScratchFileCreationHelper() { + }); + + + public boolean prepareText(@NotNull Project project, @NotNull Context context, @NotNull DataContext dataContext) { + return false; + } + + public void beforeCreate(@NotNull Project project, @NotNull Context context) { + } + + public static class Context { + @NotNull + public String text = ""; + public Language language; + public int caretOffset; + + public String filePrefix; + public Factory fileCounter; + public String fileExtension; + + public ScratchFileService.Option createOption = ScratchFileService.Option.create_new_always; + } + + @Nullable + public static PsiFile parseHeader(@NotNull Project project, + @NotNull Language language, + @NotNull String text) { + LanguageFileType fileType = language.getAssociatedFileType(); + CharSequence fileSnippet = StringUtil.first(text, 10 * 1024, false); + PsiFileFactory fileFactory = PsiFileFactory.getInstance(project); + return fileFactory.createFileFromText( + PathUtil.makeFileName("a", fileType == null ? "" : fileType.getDefaultExtension()), + language, fileSnippet); + } + + @NotNull + public static String reformat(@NotNull Project project, + @NotNull Language language, + @NotNull String text) { + return WriteCommandAction.runWriteCommandAction(project, (Computable)() -> { + PsiFile psi = parseHeader(project, language, text); + if (psi != null) CodeStyleManager.getInstance(project).reformat(psi); + return psi == null ? text : psi.getText(); + }); + } +} diff --git a/platform/platform-resources/src/META-INF/LangExtensionPoints.xml b/platform/platform-resources/src/META-INF/LangExtensionPoints.xml index 83d712e91f5b..f81ea8bfa165 100644 --- a/platform/platform-resources/src/META-INF/LangExtensionPoints.xml +++ b/platform/platform-resources/src/META-INF/LangExtensionPoints.xml @@ -895,6 +895,9 @@ + + + diff --git a/resources/src/META-INF/IdeaPlugin.xml b/resources/src/META-INF/IdeaPlugin.xml index 30ad551070ed..fcabc28a88cd 100644 --- a/resources/src/META-INF/IdeaPlugin.xml +++ b/resources/src/META-INF/IdeaPlugin.xml @@ -341,6 +341,7 @@ + com.intellij.testIntegration.createTest.GenerateMissedTestsAction