From cb2d1406cceac5fd582b1b52547a819b50144094 Mon Sep 17 00:00:00 2001 From: shuzijun Date: Mon, 16 Aug 2021 17:22:29 +0800 Subject: [PATCH] Optimization view --- .github/FUNDING.yml | 12 + README.md | 5 +- README_ZH.md | 5 +- build.gradle | 7 +- gradle.properties | 2 +- .../plugin/actions/toolbar/DonateAction.java | 17 + .../plugin/editor/BaseController.java | 103 ++++ .../plugin/editor/ContentProvider.java | 16 + .../leetcode/plugin/editor/LCVFileType.java | 41 ++ .../leetcode/plugin/editor/LCVLanguage.java | 15 + .../leetcode/plugin/editor/LCVPanel.java | 158 ++++++ .../leetcode/plugin/editor/LCVPreview.java | 213 +++++++++ .../leetcode/plugin/editor/LCVProvider.java | 39 ++ .../plugin/editor/PreviewStaticServer.java | 63 +++ .../editor/ProxyLoadHtmlResourceHandler.java | 80 ++++ .../plugin/editor/QuestionEditorProvider.java | 6 +- .../editor/QuestionEditorWithPreview.java | 15 +- .../plugin/editor/ResourcesController.java | 48 ++ .../plugin/editor/SplitFileEditor.java | 451 ++++++++++++++++++ .../editor/SplitTextEditorProvider.java | 139 ++++++ .../plugin/manager/ArticleManager.java | 104 +--- .../leetcode/plugin/manager/CodeManager.java | 32 +- .../leetcode/plugin/model/Config.java | 66 ++- .../leetcode/plugin/model/PluginConstant.java | 3 + .../leetcode/plugin/setting/SettingUI.form | 20 +- .../leetcode/plugin/setting/SettingUI.java | 6 + .../leetcode/plugin/utils/FileUtils.java | 8 + .../plugin/utils/doc/CleanExtensions.java | 35 ++ .../plugin/utils/doc/CleanMarkdown.java | 34 ++ .../plugin/utils/doc/CleanNodeFormatter.java | 165 +++++++ .../plugin/utils/doc/InlineLaTexNode.java | 37 ++ .../utils/doc/InlineLaTexProcessor.java | 76 +++ .../leetcode/plugin/window/JcefLogin.java | 3 +- .../leetcode/plugin/window/WindowFactory.java | 6 +- src/main/java/icons/LeetCodeEditorIcons.java | 58 +-- src/main/resources/META-INF/plugin.xml | 14 +- src/main/resources/META-INF/pluginIcon.svg | 21 + src/main/resources/i18n/info.properties | 4 +- src/main/resources/i18n/info_zh.properties | 8 +- src/main/resources/icons/donate.svg | 7 + src/main/resources/icons/donate_dark.svg | 7 + .../resources/icons/emperor_new_clothes.svg | 4 + src/main/resources/icons/lcv.svg | 11 + src/main/resources/icons/lcv_dark.svg | 11 + src/main/resources/template/default.html | 58 +++ 45 files changed, 2042 insertions(+), 191 deletions(-) create mode 100644 .github/FUNDING.yml create mode 100644 src/main/java/com/shuzijun/leetcode/plugin/actions/toolbar/DonateAction.java create mode 100644 src/main/java/com/shuzijun/leetcode/plugin/editor/BaseController.java create mode 100644 src/main/java/com/shuzijun/leetcode/plugin/editor/ContentProvider.java create mode 100644 src/main/java/com/shuzijun/leetcode/plugin/editor/LCVFileType.java create mode 100644 src/main/java/com/shuzijun/leetcode/plugin/editor/LCVLanguage.java create mode 100644 src/main/java/com/shuzijun/leetcode/plugin/editor/LCVPanel.java create mode 100644 src/main/java/com/shuzijun/leetcode/plugin/editor/LCVPreview.java create mode 100644 src/main/java/com/shuzijun/leetcode/plugin/editor/LCVProvider.java create mode 100644 src/main/java/com/shuzijun/leetcode/plugin/editor/PreviewStaticServer.java create mode 100644 src/main/java/com/shuzijun/leetcode/plugin/editor/ProxyLoadHtmlResourceHandler.java create mode 100644 src/main/java/com/shuzijun/leetcode/plugin/editor/ResourcesController.java create mode 100644 src/main/java/com/shuzijun/leetcode/plugin/editor/SplitFileEditor.java create mode 100644 src/main/java/com/shuzijun/leetcode/plugin/editor/SplitTextEditorProvider.java create mode 100644 src/main/java/com/shuzijun/leetcode/plugin/utils/doc/CleanExtensions.java create mode 100644 src/main/java/com/shuzijun/leetcode/plugin/utils/doc/CleanMarkdown.java create mode 100644 src/main/java/com/shuzijun/leetcode/plugin/utils/doc/CleanNodeFormatter.java create mode 100644 src/main/java/com/shuzijun/leetcode/plugin/utils/doc/InlineLaTexNode.java create mode 100644 src/main/java/com/shuzijun/leetcode/plugin/utils/doc/InlineLaTexProcessor.java create mode 100644 src/main/resources/META-INF/pluginIcon.svg create mode 100644 src/main/resources/icons/donate.svg create mode 100644 src/main/resources/icons/donate_dark.svg create mode 100644 src/main/resources/icons/emperor_new_clothes.svg create mode 100644 src/main/resources/icons/lcv.svg create mode 100644 src/main/resources/icons/lcv_dark.svg create mode 100644 src/main/resources/template/default.html diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..3d83af55 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,12 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: ['https://shuzijun.cn/donate.html'] diff --git a/README.md b/README.md index c5b663f7..4416d538 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,9 @@

### Installation([help](https://www.jetbrains.com/help/idea/2019.2/managing-plugins.html)) -- **Install via plug-in library** https://plugins.jetbrains.com/plugin/12132-leetcode-editor -- **Install by downloading the file** https://github.com/shuzijun/leetcode-editor/releases +- **Install via plug-in library** [leetcode-editor](https://plugins.jetbrains.com/plugin/12132-leetcode-editor) +- **Install by downloading the file** [releases](https://github.com/shuzijun/leetcode-editor/releases) +- **If you are willing to donate to this project, you can choose the pro version** [leetcode-editor-pro](https://plugins.jetbrains.com/plugin/17166-leetcode-editor-pro) ### Configuration (configuration for first installation) diff --git a/README_ZH.md b/README_ZH.md index ccf49dfb..1518ebcf 100644 --- a/README_ZH.md +++ b/README_ZH.md @@ -24,8 +24,9 @@

### 安装 -- **通过插件库安装** https://plugins.jetbrains.com/plugin/12132-leetcode-editor -- **下载文件安装** https://github.com/shuzijun/leetcode-editor/releases +- **通过插件库安装** [leetcode-editor](https://plugins.jetbrains.com/plugin/12132-leetcode-editor) +- **下载文件安装** [releases](https://github.com/shuzijun/leetcode-editor/releases) +- **如果您想捐助此项目,可以选择pro版本** [leetcode-editor-pro](https://plugins.jetbrains.com/plugin/17166-leetcode-editor-pro) ### 配置(第一次安装需要先配置) diff --git a/build.gradle b/build.gradle index 9af7f48c..5d6e299c 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,8 @@ dependencies { } api 'org.scilab.forge:jlatexmath:1.0.7' api 'org.apache.commons:commons-lang3:3.9' + api 'com.vladsch.flexmark:flexmark:0.62.2' + api 'com.vladsch.flexmark:flexmark-ext-attributes:0.62.2' //api fileTree(dir: 'src/main/resources/lib', include: ['*.jar']) } @@ -37,7 +39,10 @@ intellij { ideaDependencyCachePath = "$gradle.gradleUserHomeDir/caches/modules-2/files-2.1/com.jetbrains.intellij.idea" /* localPath project.idea_local_path alternativeIdePath project.idea_local_path*/ - plugins = project.intellij_plugins.split(",").toList() + /*plugins = project.intellij_plugins.split(",").toList()*/ + runIde { + jvmArgs = ["-Dfile.encoding=utf-8"] + } /* prepareSandbox { from("src/main/natives" + (project.build_env.isEmpty() ? "" : "-") + project.build_env) { into(getPluginName() + '/natives') } }*/ diff --git a/gradle.properties b/gradle.properties index 85189b0c..ee56b54f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -plugin_version = 2021.1.0 +plugin_version = 7.0 idea_local_path = # '' | windows | mac | linux build_env = diff --git a/src/main/java/com/shuzijun/leetcode/plugin/actions/toolbar/DonateAction.java b/src/main/java/com/shuzijun/leetcode/plugin/actions/toolbar/DonateAction.java new file mode 100644 index 00000000..0bbe31be --- /dev/null +++ b/src/main/java/com/shuzijun/leetcode/plugin/actions/toolbar/DonateAction.java @@ -0,0 +1,17 @@ +package com.shuzijun.leetcode.plugin.actions.toolbar; + +import com.intellij.ide.BrowserUtil; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; + + +/** + * @author shuzijun + */ +public class DonateAction extends AnAction { + @Override + public void actionPerformed(AnActionEvent anActionEvent) { + BrowserUtil.browse("https://shuzijun.cn/donate.html"); + } + +} diff --git a/src/main/java/com/shuzijun/leetcode/plugin/editor/BaseController.java b/src/main/java/com/shuzijun/leetcode/plugin/editor/BaseController.java new file mode 100644 index 00000000..bea2959c --- /dev/null +++ b/src/main/java/com/shuzijun/leetcode/plugin/editor/BaseController.java @@ -0,0 +1,103 @@ +package com.shuzijun.leetcode.plugin.editor; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.io.Responses; + +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; + +/** + * @author shuzijun + */ +public abstract class BaseController { + + // every time the plugin starts up, assume resources could have been modified + protected static final long LAST_MODIFIED = System.currentTimeMillis(); + + public final void process(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) throws IOException { + FullHttpResponse response; + try { + if (request.method() == HttpMethod.POST) { + response = post(urlDecoder, request, context); + } else if (request.method() == HttpMethod.GET) { + response = get(urlDecoder, request, context); + } else { + response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND); + } + } catch (Throwable t) { + response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR, Unpooled.wrappedBuffer(t.getMessage().getBytes(StandardCharsets.UTF_8))); + } + Responses.send(response, context.channel(), request); + if (response.content() != Unpooled.EMPTY_BUFFER) { + try { + response.release(); + } catch (Exception ignore) { + } + } + } + + public FullHttpResponse get(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) throws IOException { + return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND); + } + + public FullHttpResponse post(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) throws IOException { + return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND); + } + + public abstract String getControllerPath(); + + protected String getResourceName(QueryStringDecoder urlDecoder) { + return urlDecoder.path().substring(PreviewStaticServer.PREFIX.length() + getControllerPath().length()); + } + + protected String getParameter(@NotNull QueryStringDecoder urlDecoder, @NotNull String parameter) { + List parameters = urlDecoder.parameters().get(parameter); + if (parameters == null || parameters.size() != 1) { + return null; + } + return URLDecoder.decode(parameters.get(0), StandardCharsets.UTF_8); + } + + protected FullHttpResponse fillHtmlResponse(String content) { + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(content.getBytes(StandardCharsets.UTF_8))); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain;charset=UTF-8"); + response.headers().set(HttpHeaderNames.CACHE_CONTROL, "max-age=5, private, must-revalidate"); + response.headers().set("Referrer-Policy", "no-referrer"); + return response; + } + + protected FullHttpResponse fillJsonResponse(String content) { + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(content.getBytes(StandardCharsets.UTF_8))); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, "application/json;charset=UTF-8"); + response.headers().set(HttpHeaderNames.CACHE_CONTROL, "max-age=5, private, must-revalidate"); + response.headers().set("Referrer-Policy", "no-referrer"); + return response; + } + + public void addRoute(Map route) { + route.put(PreviewStaticServer.PREFIX + getControllerPath(), this); + } + + protected Project getProject(String projectNameParameter, String projectUrlParameter) { + Project project = null; + for (Project p : ProjectManager.getInstance().getOpenProjects()) { + if ((projectNameParameter != null && projectNameParameter.equals(p.getName())) + || (projectUrlParameter != null && projectUrlParameter.equals(p.getPresentableUrl()))) { + project = p; + break; + } + } + + return project; + } + + +} diff --git a/src/main/java/com/shuzijun/leetcode/plugin/editor/ContentProvider.java b/src/main/java/com/shuzijun/leetcode/plugin/editor/ContentProvider.java new file mode 100644 index 00000000..b074a18d --- /dev/null +++ b/src/main/java/com/shuzijun/leetcode/plugin/editor/ContentProvider.java @@ -0,0 +1,16 @@ +package com.shuzijun.leetcode.plugin.editor; + +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; + +/** + * @author shuzijun + */ +public class ContentProvider extends LCVProvider{ + + @Override + public boolean accept(@NotNull Project project, @NotNull VirtualFile file) { + return true; + } +} diff --git a/src/main/java/com/shuzijun/leetcode/plugin/editor/LCVFileType.java b/src/main/java/com/shuzijun/leetcode/plugin/editor/LCVFileType.java new file mode 100644 index 00000000..66189a42 --- /dev/null +++ b/src/main/java/com/shuzijun/leetcode/plugin/editor/LCVFileType.java @@ -0,0 +1,41 @@ +package com.shuzijun.leetcode.plugin.editor; + +import com.intellij.openapi.fileTypes.LanguageFileType; +import com.shuzijun.leetcode.plugin.model.PluginConstant; +import icons.LeetCodeEditorIcons; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; + +public class LCVFileType extends LanguageFileType { + public static final LCVFileType INSTANCE = new LCVFileType(); + + private LCVFileType() { + super(LCVLanguage.INSTANCE); + } + + @NotNull + @Override + public String getName() { + return PluginConstant.LEETCODE_EDITOR_VIEW+"Doc"; + } + + @NotNull + @Override + public String getDescription() { + return PluginConstant.LEETCODE_EDITOR_VIEW; + } + + @NotNull + @Override + public String getDefaultExtension() { + return PluginConstant.LEETCODE_EDITOR_VIEW; + } + + @Nullable + @Override + public Icon getIcon() { + return LeetCodeEditorIcons.LCV; + } +} \ No newline at end of file diff --git a/src/main/java/com/shuzijun/leetcode/plugin/editor/LCVLanguage.java b/src/main/java/com/shuzijun/leetcode/plugin/editor/LCVLanguage.java new file mode 100644 index 00000000..91cbb068 --- /dev/null +++ b/src/main/java/com/shuzijun/leetcode/plugin/editor/LCVLanguage.java @@ -0,0 +1,15 @@ +package com.shuzijun.leetcode.plugin.editor; + +import com.intellij.lang.Language; +import com.shuzijun.leetcode.plugin.model.PluginConstant; + +public class LCVLanguage extends Language { + + public static final String LANGUAGE_NAME = PluginConstant.LEETCODE_EDITOR_VIEW+"Doc"; + + public static final LCVLanguage INSTANCE = new LCVLanguage(); + + protected LCVLanguage() { + super(LANGUAGE_NAME); + } +} diff --git a/src/main/java/com/shuzijun/leetcode/plugin/editor/LCVPanel.java b/src/main/java/com/shuzijun/leetcode/plugin/editor/LCVPanel.java new file mode 100644 index 00000000..3f3f461e --- /dev/null +++ b/src/main/java/com/shuzijun/leetcode/plugin/editor/LCVPanel.java @@ -0,0 +1,158 @@ +package com.shuzijun.leetcode.plugin.editor; + +import com.intellij.ide.BrowserUtil; +import com.intellij.notification.Notification; +import com.intellij.notification.NotificationType; +import com.intellij.notification.Notifications; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.fileTypes.FileTypes; +import com.intellij.openapi.fileTypes.ex.FileTypeChooser; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.LocalFileSystem; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.jcef.JCEFHtmlPanel; +import com.intellij.util.io.HttpRequests; +import com.intellij.util.io.URLUtil; +import com.intellij.util.ui.UIUtil; +import com.shuzijun.leetcode.plugin.model.PluginConstant; +import com.shuzijun.leetcode.plugin.utils.FileUtils; +import io.netty.handler.codec.http.HttpHeaderNames; +import org.apache.commons.lang.StringUtils; +import org.cef.browser.CefBrowser; +import org.cef.browser.CefFrame; +import org.cef.handler.*; +import org.cef.misc.BoolRef; +import org.cef.network.CefRequest; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.IOException; +import java.net.HttpURLConnection; +import java.net.URLDecoder; +import java.nio.charset.StandardCharsets; +import java.util.*; + +/** + * @author shuzijun + */ +public class LCVPanel extends JCEFHtmlPanel { + + private static final Logger LOG = Logger.getInstance(LCVPanel.class); + + private final CefRequestHandler requestHandler; + private final CefLifeSpanHandler lifeSpanHandler; + + private final String url; + private final Project project; + private final List iframe = new ArrayList<>(); + private static final List headers = Arrays.asList(HttpHeaderNames.CONTENT_SECURITY_POLICY.toString(), HttpHeaderNames.CONTENT_ENCODING.toString() + , HttpHeaderNames.CONTENT_LENGTH.toString()); + + public LCVPanel(@Nullable String url, Project project) { + super("about:blank"); + this.url = url; + this.project = project; + getJBCefClient().addRequestHandler(requestHandler = new CefRequestHandlerAdapter() { + @Override + public boolean onBeforeBrowse(CefBrowser browser, CefFrame frame, CefRequest request, boolean user_gesture, boolean is_redirect) { + String requestUrl = request.getURL(); + if (requestUrl.startsWith(url)) { + return false; + } else if (!user_gesture) { + iframe.add(requestUrl); + return false; + } else { + openUrl(URLDecoder.decode(requestUrl, StandardCharsets.UTF_8)); + return true; + } + } + + @Override + public CefResourceRequestHandler getResourceRequestHandler(CefBrowser browser, CefFrame frame, CefRequest request, boolean isNavigation, boolean isDownload, String requestInitiator, BoolRef disableDefaultHandling) { + String requestUrl = request.getURL(); + if (!iframe.contains(requestUrl)) { + return null; + } + + return new CefResourceRequestHandlerAdapter() { + + @Override + public CefResourceHandler getResourceHandler(CefBrowser browser, CefFrame frame, CefRequest request) { + try { + return HttpRequests.request(request.getURL()) + .throwStatusCodeException(false) + .connect(new HttpRequests.RequestProcessor() { + @Override + public CefResourceHandler process(HttpRequests.@NotNull Request request) throws IOException { + HttpURLConnection urlConnection = (HttpURLConnection) request.getConnection(); + Map header = new HashMap<>(); + urlConnection.getHeaderFields().forEach((key, values) -> { + if (key != null && values != null && !headers.contains(key.toLowerCase())) { + header.put(key, StringUtils.join(values.toArray(), ";")); + } + }); + return new ProxyLoadHtmlResourceHandler(request.readString(), header, urlConnection.getResponseCode()); + } + }); + } catch (IOException io) { + + return null; + } + } + }; + } + }, getCefBrowser()); + getJBCefClient().addLifeSpanHandler(lifeSpanHandler = new CefLifeSpanHandlerAdapter() { + @Override + public boolean onBeforePopup(CefBrowser browser, CefFrame frame, String target_url, String target_frame_name) { + if (!target_url.startsWith(url)) { + openUrl(URLDecoder.decode(target_url, StandardCharsets.UTF_8)); + } + return true; + } + }, getCefBrowser()); + } + + @Override + public void dispose() { + getJBCefClient().removeRequestHandler(requestHandler, getCefBrowser()); + getJBCefClient().removeLifeSpanHandler(lifeSpanHandler, getCefBrowser()); + super.dispose(); + } + + private void openUrl(String url) { + if (url.startsWith(URLUtil.FILE_PROTOCOL)) { + File file = new File(url.substring((URLUtil.FILE_PROTOCOL + URLUtil.SCHEME_SEPARATOR + FileUtils.separator()).length())); + if (!file.exists()) { + Notifications.Bus.notify(new Notification(PluginConstant.NOTIFICATION_GROUP, "Cannot Open File", file.getPath() + " not exist", NotificationType.INFORMATION), project); + } else if (file.isDirectory()) { + Notifications.Bus.notify(new Notification(PluginConstant.NOTIFICATION_GROUP, "Cannot Open Directory", file.getPath() + " is a directory", NotificationType.INFORMATION), project); + } else { + ApplicationManager.getApplication().invokeLater(() -> { + VirtualFile vf = LocalFileSystem.getInstance().refreshAndFindFileByIoFile(file); + FileEditor[] editors = FileEditorManager.getInstance(project).openFile(vf, false); + if (editors == null || editors.length == 0) { + FileType fileType = FileTypeChooser.getKnownFileTypeOrAssociate(vf, project); + if (fileType == null || fileType == FileTypes.UNKNOWN) { + return; + } else { + FileEditorManager.getInstance(project).openFile(vf, false); + } + } + }); + } + } else { + BrowserUtil.browse(url); + } + } + + public void updateStyle(String style) { + getCefBrowser().executeJavaScript( + "updateStyle('" + style + "'," + UIUtil.isUnderDarcula() + ");", getCefBrowser().getURL(), 0); + } +} \ No newline at end of file diff --git a/src/main/java/com/shuzijun/leetcode/plugin/editor/LCVPreview.java b/src/main/java/com/shuzijun/leetcode/plugin/editor/LCVPreview.java new file mode 100644 index 00000000..dd635149 --- /dev/null +++ b/src/main/java/com/shuzijun/leetcode/plugin/editor/LCVPreview.java @@ -0,0 +1,213 @@ +package com.shuzijun.leetcode.plugin.editor; + +import com.google.common.net.UrlEscapers; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.colors.EditorColors; +import com.intellij.openapi.editor.colors.EditorColorsListener; +import com.intellij.openapi.editor.colors.EditorColorsManager; +import com.intellij.openapi.editor.colors.EditorColorsScheme; +import com.intellij.openapi.editor.colors.impl.EditorColorsSchemeImpl; +import com.intellij.openapi.editor.markup.TextAttributes; +import com.intellij.openapi.fileEditor.FileDocumentManager; +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorLocation; +import com.intellij.openapi.fileEditor.FileEditorState; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.UserDataHolderBase; +import com.intellij.openapi.util.io.FileUtilRt; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.JBColor; +import com.intellij.ui.components.JBLabel; +import com.intellij.util.Url; +import com.intellij.util.Urls; +import com.intellij.util.io.URLUtil; +import com.intellij.util.messages.MessageBusConnection; +import com.intellij.util.ui.UIUtil; +import com.shuzijun.leetcode.plugin.model.PluginConstant; +import com.shuzijun.leetcode.plugin.utils.FileUtils; +import com.shuzijun.leetcode.plugin.utils.PropertiesUtils; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.ide.BuiltInServerManager; + +import javax.swing.*; +import java.awt.*; +import java.beans.PropertyChangeListener; +import java.io.IOException; +import java.io.InputStream; +import java.text.DecimalFormat; + +/** + * @author shuzijun + */ +public class LCVPreview extends UserDataHolderBase implements FileEditor { + + private final Project myProject; + private final VirtualFile myFile; + private final Document myDocument; + + private final JPanel myHtmlPanelWrapper; + private final LCVPanel myPanel; + + private final Url servicePath = BuiltInServerManager.getInstance().addAuthToken(Urls.parseEncoded("http://localhost:" + BuiltInServerManager.getInstance().getPort() + PreviewStaticServer.PREFIX)); + private final String templateHtmlFile = "template/default.html"; + private final boolean isPresentableUrl; + + public LCVPreview(@NotNull Project project, @NotNull VirtualFile file) { + myProject = project; + myFile = file; + myDocument = FileDocumentManager.getInstance().getDocument(myFile); + myHtmlPanelWrapper = new JPanel(new BorderLayout()); + isPresentableUrl = project.getPresentableUrl() != null; + String url = UrlEscapers.urlFragmentEscaper().escape(URLUtil.FILE_PROTOCOL + URLUtil.SCHEME_SEPARATOR + FileUtils.separator() + myFile.getPath()); + LCVPanel tempPanel = null; + try { + tempPanel = new LCVPanel(url, project); + tempPanel.loadHTML(createHtml(isPresentableUrl), url); + myHtmlPanelWrapper.add(tempPanel.getComponent(), BorderLayout.CENTER); + } catch (IllegalStateException e) { + myHtmlPanelWrapper.add(new JBLabel(e.getMessage()), BorderLayout.CENTER); + } + myPanel = tempPanel; + myHtmlPanelWrapper.repaint(); + MessageBusConnection settingsConnection = ApplicationManager.getApplication().getMessageBus().connect(this); + settingsConnection.subscribe(EditorColorsManager.TOPIC, new EditorColorsListener() { + @Override + public void globalSchemeChange(@Nullable EditorColorsScheme scheme) { + myPanel.updateStyle(getStyle(false)); + } + }); + } + + @Override + public @NotNull JComponent getComponent() { + return myHtmlPanelWrapper; + } + + @Override + public @Nullable JComponent getPreferredFocusedComponent() { + return myPanel != null ? myPanel.getComponent() : null; + } + + @Override + public @NotNull String getName() { + return PluginConstant.LEETCODE_EDITOR_VIEW; + } + + @Override + public void setState(@NotNull FileEditorState state) { + + } + + @Override + public boolean isModified() { + return false; + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) { + + } + + @Override + public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) { + + } + + @Override + public @Nullable FileEditorLocation getCurrentLocation() { + return null; + } + + @Override + public void dispose() { + if (myPanel != null) { + Disposer.dispose(myPanel); + } + } + + private String createHtml(boolean isPresentableUrl) { + InputStream inputStream = null; + + try { + + inputStream = PreviewStaticServer.class.getResourceAsStream("/" + templateHtmlFile); + + String template = new String(FileUtilRt.loadBytes(inputStream)); + return template.replace("{{service}}", servicePath.getScheme() + URLUtil.SCHEME_SEPARATOR + servicePath.getAuthority() + servicePath.getPath()) + .replace("{{serverToken}}", StringUtils.isNotBlank(servicePath.getParameters()) ? servicePath.getParameters().substring(1) : "") + .replace("{{fileValue}}", FileDocumentManager.getInstance().getDocument(myFile).getText()) + .replace("{{Lang}}", PropertiesUtils.getInfo("Lang")) + .replace("{{darcula}}", UIUtil.isUnderDarcula() + "") + .replace("{{ideStyle}}", getStyle(true)) + ; + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException ignore) { + } + } + } + } + + @Override + public @Nullable VirtualFile getFile() { + return myFile; + } + + + private String getStyle(boolean isTag) { + try { + EditorColorsSchemeImpl editorColorsScheme = (EditorColorsSchemeImpl) EditorColorsManager.getInstance().getGlobalScheme(); + Color defaultBackground = editorColorsScheme.getDefaultBackground(); + + Color scrollbarThumbColor = EditorColors.SCROLLBAR_THUMB_COLOR.getDefaultColor(); + if (editorColorsScheme.getColor(EditorColors.SCROLLBAR_THUMB_COLOR) != null) { + scrollbarThumbColor = editorColorsScheme.getColor(EditorColors.SCROLLBAR_THUMB_COLOR); + } + TextAttributes textAttributes = editorColorsScheme.getDirectlyDefinedAttributes().get("TEXT"); + Color text = null; + if (textAttributes != null) { + text = textAttributes.getForegroundColor(); + } + String fontFamily = "font-family:\""+editorColorsScheme.getEditorFontName()+"\",\"Helvetica Neue\",\"Luxi Sans\",\"DejaVu Sans\"," + + "\"Hiragino Sans GB\",\"Microsoft Yahei\",sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Noto Color Emoji\",\"Segoe UI Symbol\"," + + "\"Android Emoji\",\"EmojiSymbols\";"; + StringBuilder sb = new StringBuilder(isTag ? "" : ""); + return sb.toString(); + } catch (Exception e) { + return ""; + } + + } + + private String toHexColor(Color color) { + DecimalFormat df = new DecimalFormat("0.00"); + return String.format("rgba(%s,%s,%s,%s)", color.getRed(), color.getGreen(), color.getBlue(), df.format(color.getAlpha() / (float) 255)); + } + +} diff --git a/src/main/java/com/shuzijun/leetcode/plugin/editor/LCVProvider.java b/src/main/java/com/shuzijun/leetcode/plugin/editor/LCVProvider.java new file mode 100644 index 00000000..34acc0b2 --- /dev/null +++ b/src/main/java/com/shuzijun/leetcode/plugin/editor/LCVProvider.java @@ -0,0 +1,39 @@ +package com.shuzijun.leetcode.plugin.editor; + +import com.intellij.openapi.fileEditor.FileEditor; +import com.intellij.openapi.fileEditor.FileEditorPolicy; +import com.intellij.openapi.fileEditor.WeighedFileEditorProvider; +import com.intellij.openapi.fileTypes.FileType; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.ui.jcef.JBCefApp; +import com.shuzijun.leetcode.plugin.model.PluginConstant; +import org.jetbrains.annotations.NotNull; + +/** + * @author shuzijun + */ +public class LCVProvider extends WeighedFileEditorProvider { + + + @Override + public boolean accept(@NotNull Project project, @NotNull VirtualFile file) { + FileType fileType = file.getFileType(); + return fileType == LCVFileType.INSTANCE && JBCefApp.isSupported(); + } + + @Override + public @NotNull FileEditor createEditor(@NotNull Project project, @NotNull VirtualFile file) { + return new LCVPreview(project, file); + } + + @Override + public @NotNull String getEditorTypeId() { + return PluginConstant.LEETCODE_EDITOR_VIEW + " view"; + } + + @Override + public @NotNull FileEditorPolicy getPolicy() { + return FileEditorPolicy.HIDE_DEFAULT_EDITOR; + } +} diff --git a/src/main/java/com/shuzijun/leetcode/plugin/editor/PreviewStaticServer.java b/src/main/java/com/shuzijun/leetcode/plugin/editor/PreviewStaticServer.java new file mode 100644 index 00000000..3b91165a --- /dev/null +++ b/src/main/java/com/shuzijun/leetcode/plugin/editor/PreviewStaticServer.java @@ -0,0 +1,63 @@ +package com.shuzijun.leetcode.plugin.editor; + +import com.intellij.openapi.diagnostic.Logger; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.HttpMethod; +import io.netty.handler.codec.http.HttpRequest; +import io.netty.handler.codec.http.QueryStringDecoder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.ide.HttpRequestHandler; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * @author shuzijun + */ +public class PreviewStaticServer extends HttpRequestHandler { + + private static final Logger LOG = Logger.getInstance(PreviewStaticServer.class); + + public static final String PREFIX = "/3e41525e-e74a-4590-ba1d-982b8f638478/"; + + public static final Map route = new HashMap<>(); + static { + new ResourcesController().addRoute(route); + } + + public static PreviewStaticServer getInstance() { + return HttpRequestHandler.Companion.getEP_NAME().findExtension(PreviewStaticServer.class); + } + + @Override + public boolean isAccessible(@NotNull HttpRequest request) { + return true; + } + + @Override + public boolean isSupported(@NotNull FullHttpRequest request) { + return (request.method() == HttpMethod.GET || request.method() == HttpMethod.HEAD || request.method() == HttpMethod.POST) && request.uri().startsWith(PREFIX); + } + + @Override + public boolean process(@NotNull QueryStringDecoder urlDecoder, + @NotNull FullHttpRequest request, + @NotNull ChannelHandlerContext context) throws IOException { + final String path = urlDecoder.path(); + if (!path.startsWith(PREFIX)) { + throw new IllegalStateException("prefix should have been checked by #isSupported"); + } + + for (String controllerPath : route.keySet()) { + if (path.startsWith(controllerPath)) { + route.get(controllerPath).process(urlDecoder, request, context); + return true; + } + } + return false; + } + + +} \ No newline at end of file diff --git a/src/main/java/com/shuzijun/leetcode/plugin/editor/ProxyLoadHtmlResourceHandler.java b/src/main/java/com/shuzijun/leetcode/plugin/editor/ProxyLoadHtmlResourceHandler.java new file mode 100644 index 00000000..00796051 --- /dev/null +++ b/src/main/java/com/shuzijun/leetcode/plugin/editor/ProxyLoadHtmlResourceHandler.java @@ -0,0 +1,80 @@ +package com.shuzijun.leetcode.plugin.editor; + +import com.intellij.openapi.diagnostic.Logger; +import io.netty.handler.codec.http.HttpHeaderNames; +import org.cef.callback.CefCallback; +import org.cef.handler.CefResourceHandlerAdapter; +import org.cef.misc.IntRef; +import org.cef.misc.StringRef; +import org.cef.network.CefRequest; +import org.cef.network.CefResponse; +import org.jetbrains.annotations.NotNull; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Map; + +/** + * @author shuzijun + */ +public class ProxyLoadHtmlResourceHandler extends CefResourceHandlerAdapter { + private static final Logger LOG = Logger.getInstance(ProxyLoadHtmlResourceHandler.class.getName()); + + @NotNull + private final InputStream myInputStream; + private final Map header ; + private final int status; + + public ProxyLoadHtmlResourceHandler(@NotNull String html, Map header, int status) { + myInputStream = new ByteArrayInputStream(html.getBytes(StandardCharsets.UTF_8)); + this.header = header; + this.status = status; + } + + @Override + public boolean processRequest(@NotNull CefRequest request, @NotNull CefCallback callback) { + callback.Continue(); + return true; + } + + @Override + public void getResponseHeaders(@NotNull CefResponse response, IntRef response_length, StringRef redirectUrl) { + header.forEach((key,value) -> { + response.setHeaderByName(key,value,true); + if(HttpHeaderNames.CONTENT_TYPE.toString().equals(key.toLowerCase())){ + if(value.indexOf(";")>0){ + response.setMimeType(value.substring(0,value.indexOf(";"))); + }else { + response.setMimeType(value); + } + } + }); + response.setStatus(status); + } + + @Override + public boolean readResponse(byte@NotNull[] data_out, int bytes_to_read, IntRef bytes_read, CefCallback callback) { + try { + int availableSize = myInputStream.available(); + if (availableSize > 0) { + int bytesToRead = Math.min(bytes_to_read, availableSize); + bytesToRead = myInputStream.read(data_out, 0, bytesToRead); + bytes_read.set(bytesToRead); + return true; + } + } + catch (IOException e) { + LOG.error(e); + } + bytes_read.set(0); + try { + myInputStream.close(); + } + catch (IOException e) { + LOG.error(e); + } + return false; + } +} \ No newline at end of file diff --git a/src/main/java/com/shuzijun/leetcode/plugin/editor/QuestionEditorProvider.java b/src/main/java/com/shuzijun/leetcode/plugin/editor/QuestionEditorProvider.java index 4ff01b2b..965e0466 100644 --- a/src/main/java/com/shuzijun/leetcode/plugin/editor/QuestionEditorProvider.java +++ b/src/main/java/com/shuzijun/leetcode/plugin/editor/QuestionEditorProvider.java @@ -11,8 +11,6 @@ import com.shuzijun.leetcode.plugin.setting.PersistentConfig; import com.shuzijun.leetcode.plugin.setting.ProjectConfig; import org.apache.commons.lang3.StringUtils; -import org.intellij.plugins.markdown.ui.preview.MarkdownPreviewFileEditorProvider; -import org.intellij.plugins.markdown.ui.split.SplitTextEditorProvider; import org.jetbrains.annotations.NotNull; import java.io.File; @@ -22,10 +20,8 @@ */ public class QuestionEditorProvider extends SplitTextEditorProvider { - - public QuestionEditorProvider() { - super(new PsiAwareTextEditorProvider(), new MarkdownPreviewFileEditorProvider()); + super(new PsiAwareTextEditorProvider(), new ContentProvider()); } @Override diff --git a/src/main/java/com/shuzijun/leetcode/plugin/editor/QuestionEditorWithPreview.java b/src/main/java/com/shuzijun/leetcode/plugin/editor/QuestionEditorWithPreview.java index d410b79f..359b4f10 100644 --- a/src/main/java/com/shuzijun/leetcode/plugin/editor/QuestionEditorWithPreview.java +++ b/src/main/java/com/shuzijun/leetcode/plugin/editor/QuestionEditorWithPreview.java @@ -2,35 +2,24 @@ import com.intellij.openapi.actionSystem.ActionGroup; import com.intellij.openapi.actionSystem.ActionManager; -import com.intellij.openapi.editor.Editor; import com.intellij.openapi.fileEditor.FileEditor; import com.intellij.openapi.fileEditor.TextEditor; import com.intellij.openapi.fileEditor.TextEditorWithPreview; import com.intellij.openapi.util.Key; import com.shuzijun.leetcode.plugin.model.PluginConstant; -import org.intellij.plugins.markdown.ui.preview.MarkdownPreviewFileEditor; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.lang.reflect.Method; - /** * @author shuzijun */ public class QuestionEditorWithPreview extends TextEditorWithPreview { - public static final Key PARENT_SPLIT_EDITOR_KEY = Key.create("Question Split"); + public static final Key PARENT_SPLIT_EDITOR_KEY = Key.create(PluginConstant.PLUGIN_ID + "Question Split"); public QuestionEditorWithPreview(@NotNull TextEditor editor, @NotNull FileEditor preview) { - super(editor, preview, "Question "+ Layout.SHOW_EDITOR_AND_PREVIEW.getName() ,Layout.SHOW_EDITOR_AND_PREVIEW); + super(editor, preview, "Question " + Layout.SHOW_EDITOR_AND_PREVIEW.getName(), Layout.SHOW_EDITOR_AND_PREVIEW); editor.putUserData(PARENT_SPLIT_EDITOR_KEY, this); preview.putUserData(PARENT_SPLIT_EDITOR_KEY, this); - if(preview instanceof MarkdownPreviewFileEditor){ - try { - Method setMainEditor = preview.getClass().getMethod("setMainEditor", Editor.class); - setMainEditor.invoke(preview,editor.getEditor()); - }catch (Throwable ignore){ - } - } } @Nullable diff --git a/src/main/java/com/shuzijun/leetcode/plugin/editor/ResourcesController.java b/src/main/java/com/shuzijun/leetcode/plugin/editor/ResourcesController.java new file mode 100644 index 00000000..27bb2e9c --- /dev/null +++ b/src/main/java/com/shuzijun/leetcode/plugin/editor/ResourcesController.java @@ -0,0 +1,48 @@ +package com.shuzijun.leetcode.plugin.editor; + +import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.util.io.FileUtilRt; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.http.*; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.io.FileResponses; + +import java.io.IOException; +import java.io.InputStream; + +/** + * @author shuzijun + */ +public class ResourcesController extends BaseController { + + private static final Logger LOG = Logger.getInstance(ResourcesController.class); + + private final String controllerPath = "resources"; + + @Override + public String getControllerPath() { + return controllerPath; + } + + @Override + public FullHttpResponse get(@NotNull QueryStringDecoder urlDecoder, @NotNull FullHttpRequest request, @NotNull ChannelHandlerContext context) { + String resourceName = getResourceName(urlDecoder); + byte[] data; + try (InputStream inputStream = PreviewStaticServer.class.getResourceAsStream(resourceName)) { + if (inputStream == null) { + return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND, Unpooled.EMPTY_BUFFER); + } + data = FileUtilRt.loadBytes(inputStream); + } catch (IOException e) { + LOG.warn(e); + return new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.INTERNAL_SERVER_ERROR, Unpooled.EMPTY_BUFFER); + } + + FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled.wrappedBuffer(data)); + response.headers().set(HttpHeaderNames.CONTENT_TYPE, FileResponses.INSTANCE.getContentType(resourceName) + "; charset=utf-8"); + response.headers().set(HttpHeaderNames.CACHE_CONTROL, "max-age=3600, private, must-revalidate"); + response.headers().set(HttpHeaderNames.ETAG, Long.toString(LAST_MODIFIED)); + return response; + } +} diff --git a/src/main/java/com/shuzijun/leetcode/plugin/editor/SplitFileEditor.java b/src/main/java/com/shuzijun/leetcode/plugin/editor/SplitFileEditor.java new file mode 100644 index 00000000..30f1ed06 --- /dev/null +++ b/src/main/java/com/shuzijun/leetcode/plugin/editor/SplitFileEditor.java @@ -0,0 +1,451 @@ +package com.shuzijun.leetcode.plugin.editor; + +import com.intellij.codeHighlighting.BackgroundEditorHighlighter; +import com.intellij.ide.structureView.StructureViewBuilder; +import com.intellij.openapi.actionSystem.ActionGroup; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.ActionPlaces; +import com.intellij.openapi.actionSystem.ActionToolbar; +import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl; +import com.intellij.openapi.fileEditor.*; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.Pair; +import com.intellij.openapi.util.UserDataHolderBase; +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.openapi.wm.IdeFocusManager; +import com.intellij.ui.JBSplitter; +import com.intellij.util.ui.JBEmptyBorder; +import com.shuzijun.leetcode.plugin.model.PluginConstant; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.awt.*; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.util.HashMap; +import java.util.Map; + +import static com.intellij.openapi.fileEditor.TextEditorWithPreview.DEFAULT_LAYOUT_FOR_FILE; + +/** + * @author shuzijun + */ +public abstract class SplitFileEditor extends UserDataHolderBase implements FileEditor { + public static final Key PARENT_SPLIT_KEY = Key.create(PluginConstant.PLUGIN_ID + "parentSplit"); + + private static final String MY_PROPORTION_KEY = PluginConstant.PLUGIN_ID + "SplitFileEditor.Proportion"; + + @NotNull + protected final E1 myMainEditor; + @NotNull + protected final E2 mySecondEditor; + @NotNull + private final JComponent myComponent; + @NotNull + private SplitFileEditor.SplitEditorLayout mySplitEditorLayout = SplitEditorLayout.SPLIT; + + private boolean myVerticalSplitOption = true; + @NotNull + private final SplitFileEditor.MyListenersMultimap myListenersGenerator = new SplitFileEditor.MyListenersMultimap(); + + private SplitEditorToolbar myToolbarWrapper; + private JBSplitter mySplitter; + + public SplitFileEditor(@NotNull E1 mainEditor, @NotNull E2 secondEditor) { + myMainEditor = mainEditor; + mySecondEditor = secondEditor; + + adjustDefaultLayout(mainEditor); + myComponent = createComponent(); + + if (myMainEditor instanceof TextEditor) { + myMainEditor.putUserData(PARENT_SPLIT_KEY, this); + } + if (mySecondEditor instanceof TextEditor) { + mySecondEditor.putUserData(PARENT_SPLIT_KEY, this); + } + + /* MarkdownApplicationSettings.SettingsChangedListener settingsChangedListener = + new MarkdownApplicationSettings.SettingsChangedListener() { + @Override + public void beforeSettingsChanged(@NotNull MarkdownApplicationSettings newSettings) { + SplitFileEditor.SplitEditorLayout oldSplitEditorLayout = + MarkdownApplicationSettings.getInstance().getMarkdownPreviewSettings().getSplitEditorLayout(); + + boolean oldVerticalSplitOption = + MarkdownApplicationSettings.getInstance().getMarkdownPreviewSettings().isVerticalSplit(); + + ApplicationManager.getApplication().invokeLater(() -> { + if (oldSplitEditorLayout == mySplitEditorLayout) { + triggerLayoutChange(newSettings.getMarkdownPreviewSettings().getSplitEditorLayout(), false); + } + + if (oldVerticalSplitOption == myVerticalSplitOption) { + triggerSplitOrientationChange(newSettings.getMarkdownPreviewSettings().isVerticalSplit()); + } + }); + } + }; + + ApplicationManager.getApplication().getMessageBus().connect(this) + .subscribe(MarkdownApplicationSettings.SettingsChangedListener.TOPIC, settingsChangedListener);*/ + } + + private void adjustDefaultLayout(E1 editor) { + TextEditorWithPreview.Layout layout = getAndResetPredefinedLayoutForEditor(editor); + if (layout != null) { + switch (layout) { + case SHOW_EDITOR: + mySplitEditorLayout = SplitFileEditor.SplitEditorLayout.FIRST; + break; + case SHOW_PREVIEW: + mySplitEditorLayout = SplitFileEditor.SplitEditorLayout.SECOND; + break; + case SHOW_EDITOR_AND_PREVIEW: + mySplitEditorLayout = SplitFileEditor.SplitEditorLayout.SPLIT; + break; + } + } + } + + //todo: Refactor Markdown editor and make it a subclass of TextEditorWithPreview. + // Move this method to TextEditorWithPreview. + @Nullable + private static TextEditorWithPreview.Layout getAndResetPredefinedLayoutForEditor(FileEditor editor) { + VirtualFile file = editor.getFile(); + if (file != null) { + TextEditorWithPreview.Layout layout = file.getUserData(DEFAULT_LAYOUT_FOR_FILE); + if (layout != null) { + file.putUserData(DEFAULT_LAYOUT_FOR_FILE, null); //burn after reading + return layout; + } + } + + return null; + } + + private void triggerSplitOrientationChange(boolean isVerticalSplit) { + if (myVerticalSplitOption == isVerticalSplit) { + return; + } + + myVerticalSplitOption = isVerticalSplit; + + myToolbarWrapper.refresh(); + mySplitter.setOrientation(!myVerticalSplitOption); + myComponent.repaint(); + } + + @NotNull + private JComponent createComponent() { + mySplitter = + new JBSplitter(!myVerticalSplitOption, 0.5f, 0.15f, 0.85f); + mySplitter.setSplitterProportionKey(MY_PROPORTION_KEY); + mySplitter.setFirstComponent(myMainEditor.getComponent()); + mySplitter.setSecondComponent(mySecondEditor.getComponent()); + mySplitter.setDividerWidth(3); + + myToolbarWrapper = createMarkdownToolbarWrapper(mySplitter); + + final JPanel result = new JPanel(new BorderLayout()); + result.add(myToolbarWrapper, BorderLayout.NORTH); + result.add(mySplitter, BorderLayout.CENTER); + adjustEditorsVisibility(); + + return result; + } + + @NotNull + private static SplitEditorToolbar createMarkdownToolbarWrapper(@NotNull JComponent targetComponentForActions) { + ActionToolbar leftToolbar = createToolbarFromGroupId("Markdown.Toolbar.Left"); + leftToolbar.setTargetComponent(targetComponentForActions); + leftToolbar.setReservePlaceAutoPopupIcon(false); + + ActionToolbar rightToolbar = createToolbarFromGroupId("Markdown.Toolbar.Right"); + rightToolbar.setTargetComponent(targetComponentForActions); + rightToolbar.setReservePlaceAutoPopupIcon(false); + + return new SplitEditorToolbar(leftToolbar, rightToolbar); + } + + @NotNull + private static ActionToolbar createToolbarFromGroupId(@NotNull String groupId) { + final ActionManager actionManager = ActionManager.getInstance(); + + if (!actionManager.isGroup(groupId)) { + throw new IllegalStateException(groupId + " should have been a group"); + } + final ActionGroup group = ((ActionGroup) actionManager.getAction(groupId)); + final ActionToolbarImpl editorToolbar = + ((ActionToolbarImpl) actionManager.createActionToolbar(ActionPlaces.EDITOR_TOOLBAR, group, true)); + editorToolbar.setBorder(new JBEmptyBorder(0, 2, 0, 2)); + + return editorToolbar; + } + + public void triggerLayoutChange() { + final int oldValue = mySplitEditorLayout.ordinal(); + final int N = SplitFileEditor.SplitEditorLayout.values().length; + final int newValue = (oldValue + N - 1) % N; + + triggerLayoutChange(SplitFileEditor.SplitEditorLayout.values()[newValue], true); + } + + public void triggerLayoutChange(@NotNull SplitFileEditor.SplitEditorLayout newLayout, boolean requestFocus) { + if (mySplitEditorLayout == newLayout) { + return; + } + + mySplitEditorLayout = newLayout; + invalidateLayout(requestFocus); + } + + @NotNull + public SplitFileEditor.SplitEditorLayout getCurrentEditorLayout() { + return mySplitEditorLayout; + } + + private void invalidateLayout(boolean requestFocus) { + adjustEditorsVisibility(); + myToolbarWrapper.refresh(); + myComponent.repaint(); + + if (!requestFocus) return; + + final JComponent focusComponent = getPreferredFocusedComponent(); + if (focusComponent != null) { + IdeFocusManager.findInstanceByComponent(focusComponent).requestFocus(focusComponent, true); + } + } + + private void adjustEditorsVisibility() { + myMainEditor.getComponent().setVisible(mySplitEditorLayout.showFirst); + mySecondEditor.getComponent().setVisible(mySplitEditorLayout.showSecond); + } + + @NotNull + public E1 getMainEditor() { + return myMainEditor; + } + + @NotNull + public E2 getSecondEditor() { + return mySecondEditor; + } + + @NotNull + @Override + public JComponent getComponent() { + return myComponent; + } + + @Nullable + @Override + public JComponent getPreferredFocusedComponent() { + if (mySplitEditorLayout.showFirst) return myMainEditor.getPreferredFocusedComponent(); + if (mySplitEditorLayout.showSecond) return mySecondEditor.getPreferredFocusedComponent(); + return null; + } + + @NotNull + @Override + public FileEditorState getState(@NotNull FileEditorStateLevel level) { + return new SplitFileEditor.MyFileEditorState(mySplitEditorLayout.name(), myMainEditor.getState(level), mySecondEditor.getState(level)); + } + + @Override + public void setState(@NotNull FileEditorState state) { + if (state instanceof SplitFileEditor.MyFileEditorState) { + final SplitFileEditor.MyFileEditorState compositeState = (SplitFileEditor.MyFileEditorState) state; + if (compositeState.getFirstState() != null) { + myMainEditor.setState(compositeState.getFirstState()); + } + if (compositeState.getSecondState() != null) { + mySecondEditor.setState(compositeState.getSecondState()); + } + if (compositeState.getSplitLayout() != null) { + mySplitEditorLayout = SplitFileEditor.SplitEditorLayout.valueOf(compositeState.getSplitLayout()); + invalidateLayout(true); + } + } + } + + @Override + public boolean isModified() { + return myMainEditor.isModified() || mySecondEditor.isModified(); + } + + @Override + public boolean isValid() { + return myMainEditor.isValid() && mySecondEditor.isValid(); + } + + @Override + public void selectNotify() { + myMainEditor.selectNotify(); + mySecondEditor.selectNotify(); + } + + @Override + public void deselectNotify() { + myMainEditor.deselectNotify(); + mySecondEditor.deselectNotify(); + } + + @Override + public void addPropertyChangeListener(@NotNull PropertyChangeListener listener) { + myMainEditor.addPropertyChangeListener(listener); + mySecondEditor.addPropertyChangeListener(listener); + + final SplitFileEditor.DoublingEventListenerDelegate delegate = myListenersGenerator.addListenerAndGetDelegate(listener); + myMainEditor.addPropertyChangeListener(delegate); + mySecondEditor.addPropertyChangeListener(delegate); + } + + @Override + public void removePropertyChangeListener(@NotNull PropertyChangeListener listener) { + myMainEditor.removePropertyChangeListener(listener); + mySecondEditor.removePropertyChangeListener(listener); + + final SplitFileEditor.DoublingEventListenerDelegate delegate = myListenersGenerator.removeListenerAndGetDelegate(listener); + if (delegate != null) { + myMainEditor.removePropertyChangeListener(delegate); + mySecondEditor.removePropertyChangeListener(delegate); + } + } + + @Nullable + @Override + public BackgroundEditorHighlighter getBackgroundHighlighter() { + return myMainEditor.getBackgroundHighlighter(); + } + + @Nullable + @Override + public FileEditorLocation getCurrentLocation() { + return myMainEditor.getCurrentLocation(); + } + + @Nullable + @Override + public StructureViewBuilder getStructureViewBuilder() { + return myMainEditor.getStructureViewBuilder(); + } + + @Override + public void dispose() { + Disposer.dispose(myMainEditor); + Disposer.dispose(mySecondEditor); + } + + public static class MyFileEditorState implements FileEditorState { + @Nullable + private final String mySplitLayout; + @Nullable + private final FileEditorState myFirstState; + @Nullable + private final FileEditorState mySecondState; + + public MyFileEditorState(@Nullable String splitLayout, @Nullable FileEditorState firstState, @Nullable FileEditorState secondState) { + mySplitLayout = splitLayout; + myFirstState = firstState; + mySecondState = secondState; + } + + @Nullable + public String getSplitLayout() { + return mySplitLayout; + } + + @Nullable + public FileEditorState getFirstState() { + return myFirstState; + } + + @Nullable + public FileEditorState getSecondState() { + return mySecondState; + } + + @Override + public boolean canBeMergedWith(FileEditorState otherState, FileEditorStateLevel level) { + return otherState instanceof SplitFileEditor.MyFileEditorState + && (myFirstState == null || myFirstState.canBeMergedWith(((SplitFileEditor.MyFileEditorState) otherState).myFirstState, level)) + && (mySecondState == null || mySecondState.canBeMergedWith(((SplitFileEditor.MyFileEditorState) otherState).mySecondState, level)); + } + } + + private class DoublingEventListenerDelegate implements PropertyChangeListener { + @NotNull + private final PropertyChangeListener myDelegate; + + private DoublingEventListenerDelegate(@NotNull PropertyChangeListener delegate) { + myDelegate = delegate; + } + + @Override + public void propertyChange(PropertyChangeEvent evt) { + myDelegate.propertyChange(new PropertyChangeEvent(SplitFileEditor.this, evt.getPropertyName(), evt.getOldValue(), evt.getNewValue())); + } + } + + private class MyListenersMultimap { + private final Map> myMap = new HashMap<>(); + + @NotNull + public SplitFileEditor.DoublingEventListenerDelegate addListenerAndGetDelegate(@NotNull PropertyChangeListener listener) { + if (!myMap.containsKey(listener)) { + myMap.put(listener, Pair.create(1, new SplitFileEditor.DoublingEventListenerDelegate(listener))); + } else { + final Pair oldPair = myMap.get(listener); + myMap.put(listener, Pair.create(oldPair.getFirst() + 1, oldPair.getSecond())); + } + + return myMap.get(listener).getSecond(); + } + + @Nullable + public SplitFileEditor.DoublingEventListenerDelegate removeListenerAndGetDelegate(@NotNull PropertyChangeListener listener) { + final Pair oldPair = myMap.get(listener); + if (oldPair == null) { + return null; + } + + if (oldPair.getFirst() == 1) { + myMap.remove(listener); + } else { + myMap.put(listener, Pair.create(oldPair.getFirst() - 1, oldPair.getSecond())); + } + return oldPair.getSecond(); + } + } + + public enum SplitEditorLayout { + FIRST(true, false, "editor only"), + SECOND(false, true, "preview only"), + SPLIT(true, true, "editor and preview"); + + public final boolean showFirst; + public final boolean showSecond; + public final String presentationName; + + SplitEditorLayout(boolean showFirst, boolean showSecond, String presentationName) { + this.showFirst = showFirst; + this.showSecond = showSecond; + this.presentationName = presentationName; + } + + public String getPresentationText() { + return StringUtil.capitalize(presentationName); + } + + @Override + public String toString() { + return String.format("Show %s", presentationName); + } + } +} + diff --git a/src/main/java/com/shuzijun/leetcode/plugin/editor/SplitTextEditorProvider.java b/src/main/java/com/shuzijun/leetcode/plugin/editor/SplitTextEditorProvider.java new file mode 100644 index 00000000..1e5a22fa --- /dev/null +++ b/src/main/java/com/shuzijun/leetcode/plugin/editor/SplitTextEditorProvider.java @@ -0,0 +1,139 @@ +package com.shuzijun.leetcode.plugin.editor; + +import com.intellij.openapi.fileEditor.*; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import org.jdom.Attribute; +import org.jdom.Element; +import org.jetbrains.annotations.NotNull; + +/** + * @author shuzijun + */ +public abstract class SplitTextEditorProvider implements AsyncFileEditorProvider, DumbAware { + + private static final String FIRST_EDITOR = "first_editor"; + private static final String SECOND_EDITOR = "second_editor"; + private static final String SPLIT_LAYOUT = "split_layout"; + + @NotNull + protected final FileEditorProvider myFirstProvider; + @NotNull + protected final FileEditorProvider mySecondProvider; + + @NotNull + private final String myEditorTypeId; + + public SplitTextEditorProvider(@NotNull FileEditorProvider firstProvider, @NotNull FileEditorProvider secondProvider) { + myFirstProvider = firstProvider; + mySecondProvider = secondProvider; + + myEditorTypeId = "split-provider[" + myFirstProvider.getEditorTypeId() + ";" + mySecondProvider.getEditorTypeId() + "]"; + } + + @Override + public boolean accept(@NotNull Project project, @NotNull VirtualFile file) { + return myFirstProvider.accept(project, file) && mySecondProvider.accept(project, file); + } + + @NotNull + @Override + public FileEditor createEditor(@NotNull Project project, @NotNull VirtualFile file) { + return createEditorAsync(project, file).build(); + } + + @NotNull + @Override + public String getEditorTypeId() { + return myEditorTypeId; + } + + @NotNull + @Override + public AsyncFileEditorProvider.Builder createEditorAsync(@NotNull final Project project, @NotNull final VirtualFile file) { + final Builder firstBuilder = getBuilderFromEditorProvider(myFirstProvider, project, file); + final Builder secondBuilder = getBuilderFromEditorProvider(mySecondProvider, project, file); + + return new Builder() { + @Override + public FileEditor build() { + return createSplitEditor(firstBuilder.build(), secondBuilder.build()); + } + }; + } + + @NotNull + @Override + public FileEditorState readState(@NotNull Element sourceElement, @NotNull Project project, @NotNull VirtualFile file) { + Element child = sourceElement.getChild(FIRST_EDITOR); + FileEditorState firstState = null; + if (child != null) { + firstState = myFirstProvider.readState(child, project, file); + } + child = sourceElement.getChild(SECOND_EDITOR); + FileEditorState secondState = null; + if (child != null) { + secondState = mySecondProvider.readState(child, project, file); + } + + final Attribute attribute = sourceElement.getAttribute(SPLIT_LAYOUT); + + final String layoutName; + if (attribute != null) { + layoutName = attribute.getValue(); + } else { + layoutName = null; + } + + return new SplitFileEditor.MyFileEditorState(layoutName, firstState, secondState); + } + + @Override + public void writeState(@NotNull FileEditorState state, @NotNull Project project, @NotNull Element targetElement) { + if (!(state instanceof SplitFileEditor.MyFileEditorState)) { + return; + } + final SplitFileEditor.MyFileEditorState compositeState = (SplitFileEditor.MyFileEditorState) state; + + Element child = new Element(FIRST_EDITOR); + if (compositeState.getFirstState() != null) { + myFirstProvider.writeState(compositeState.getFirstState(), project, child); + targetElement.addContent(child); + } + + child = new Element(SECOND_EDITOR); + if (compositeState.getSecondState() != null) { + mySecondProvider.writeState(compositeState.getSecondState(), project, child); + targetElement.addContent(child); + } + + if (compositeState.getSplitLayout() != null) { + targetElement.setAttribute(SPLIT_LAYOUT, compositeState.getSplitLayout()); + } + } + + protected abstract FileEditor createSplitEditor(@NotNull FileEditor firstEditor, @NotNull FileEditor secondEditor); + + @NotNull + @Override + public FileEditorPolicy getPolicy() { + return FileEditorPolicy.HIDE_DEFAULT_EDITOR; + } + + @NotNull + public static Builder getBuilderFromEditorProvider(@NotNull final FileEditorProvider provider, + @NotNull final Project project, + @NotNull final VirtualFile file) { + if (provider instanceof AsyncFileEditorProvider) { + return ((AsyncFileEditorProvider) provider).createEditorAsync(project, file); + } else { + return new Builder() { + @Override + public FileEditor build() { + return provider.createEditor(project, file); + } + }; + } + } +} diff --git a/src/main/java/com/shuzijun/leetcode/plugin/manager/ArticleManager.java b/src/main/java/com/shuzijun/leetcode/plugin/manager/ArticleManager.java index 34dbf9b5..efe2e3d2 100644 --- a/src/main/java/com/shuzijun/leetcode/plugin/manager/ArticleManager.java +++ b/src/main/java/com/shuzijun/leetcode/plugin/manager/ArticleManager.java @@ -1,55 +1,45 @@ package com.shuzijun.leetcode.plugin.manager; -import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.intellij.openapi.project.Project; -import com.intellij.ui.JBColor; import com.shuzijun.leetcode.plugin.model.Constant; +import com.shuzijun.leetcode.plugin.model.PluginConstant; import com.shuzijun.leetcode.plugin.model.Question; import com.shuzijun.leetcode.plugin.model.Solution; import com.shuzijun.leetcode.plugin.setting.PersistentConfig; import com.shuzijun.leetcode.plugin.utils.*; +import com.shuzijun.leetcode.plugin.utils.doc.CleanMarkdown; import org.apache.commons.lang.StringUtils; -import org.jsoup.Jsoup; -import org.scilab.forge.jlatexmath.TeXConstants; -import org.scilab.forge.jlatexmath.TeXFormula; import java.io.File; import java.util.ArrayList; import java.util.List; -import java.util.regex.Matcher; -import java.util.regex.Pattern; /** * @author shuzijun */ public class ArticleManager { - private static final Pattern latexPattern = Pattern.compile("\\$+[\\s|\\S]*?\\$+"); - private static final Pattern imageListPattern = Pattern.compile("<(!\\[.*]\\(.*\\),*)+>?"); - private static final Pattern imagePattern = Pattern.compile("?"); - private static final Pattern linkPattern = Pattern.compile("]\\(\\.\\.[\\w|/]*?\\.\\w+\\)?"); - private static final Pattern gifPattern = Pattern.compile("!\\[.*?]\\(.*?\\.gif\\)?"); - private static final Pattern playgroundPattern = Pattern.compile("?"); - private static final Pattern imageGroupPattern = Pattern.compile("!\\?!\\.\\..*\\?!?"); - public static void openArticle(Question question, Project project) { - String filePath = PersistentConfig.getInstance().getTempFilePath() + Constant.DOC_SOLUTION + question.getArticleSlug() + ".md"; + String filePath = PersistentConfig.getInstance().getTempFilePath() + Constant.DOC_SOLUTION + question.getArticleSlug() + "." + PluginConstant.LEETCODE_EDITOR_VIEW; File file = new File(filePath); + String host = ""; if (!file.exists()) { String article; if (URLUtils.isCn()) { article = getCnArticle(question, project); + host = URLUtils.getLeetcodeProblems() + question.getTitleSlug() + "/solution/" + question.getArticleSlug() + "/"; } else { article = getEnArticle(question, project); + host = URLUtils.getLeetcodeProblems() + question.getTitleSlug() + "/solution/"; } if (StringUtils.isBlank(article)) { return; } - article = formatMarkdown(article, project); + article = formatMarkdown(article, project,host); FileUtils.saveFile(file, article); } @@ -112,84 +102,8 @@ private static String getCnArticle(Question question, Project project) { } - public static String formatMarkdown(String content, Project project) { - String article = content.replaceAll("\\{:\\w+=\"?.*\"?}?", ""); - Matcher latexMatcher = latexPattern.matcher(content); - while (latexMatcher.find()) { - String group = latexMatcher.group(); - if (group.contains("\\")) { - String fileName = "p_" + group.replaceAll("\\$+| |/|>|<|\\(|\\)|\\s|\\[|]", "_").replace("\\", "") + ".png"; - String filePath = PersistentConfig.getInstance().getTempFilePath() + Constant.DOC_SOLUTION + fileName; - File file = new File(filePath); - if (!file.exists()) { - if (!file.getParentFile().exists()) { - file.getParentFile().mkdirs(); - } - try { - TeXFormula formula = new TeXFormula(group.replaceAll("\\$+|\\s", " ")); - formula.createPNG(TeXConstants.STYLE_DISPLAY, 14, filePath, null, JBColor.BLACK); - } catch (Exception e) { - LogUtils.LOG.error("TeXFormula error", e); - } - } - article = article.replace(group, "![" + group.replaceAll("\\$+|\\s", "").replaceAll("([\\[\\]])", "\\\\$1") + " ](./" + fileName + ") "); - } else { - article = article.replace(group, group.replaceAll("\\$+", "*")); - } - } - Matcher imageListMatcher = imageListPattern.matcher(content); - while (imageListMatcher.find()) { - article = article.replace(imageListMatcher.group(), imageListMatcher.group().replaceAll("[<>,]"," ")); - } - - Matcher linkMatcher = linkPattern.matcher(content); - while (linkMatcher.find()) { - article = article.replace(linkMatcher.group(), linkMatcher.group().replace("..", URLUtils.getLeetcodeProblems())); - } - Matcher imageMatcher = imagePattern.matcher(content); - while (imageMatcher.find()) { - article = article.replace(imageMatcher.group(), imageMatcher.group().replace("..", URLUtils.getLeetcodeProblems())); - } - Matcher gifMatcher = gifPattern.matcher(content); - while (gifMatcher.find()) { - article = article.replace(gifMatcher.group(), gifMatcher.group().replace("!", " ")); - } - - Matcher playgroundMatcher = playgroundPattern.matcher(content); - while (playgroundMatcher.find()) { - String group = playgroundMatcher.group(); - String name = Jsoup.parse(group).select("iframe").attr("name"); - HttpRequest playgroundHttpRequest = HttpRequest.post(URLUtils.getLeetcodeGraphql(), "application/json"); - playgroundHttpRequest.setBody("{\"operationName\":\"fetchPlayground\",\"variables\":{},\"query\":\"query fetchPlayground {\\n playground(uuid: \\\"" + name + "\\\") {\\n testcaseInput\\n name\\n isUserOwner\\n" + - " isLive\\n showRunCode\\n showOpenInPlayground\\n selectedLangSlug\\n isShared\\n __typename\\n }\\n allPlaygroundCodes(uuid: \\\"" + name + "\\\") {\\n code\\n langSlug\\n __typename\\n }\\n}\\n\"}"); - playgroundHttpRequest.addHeader("Accept", "application/json"); - HttpResponse playgroundResponse = HttpRequestUtils.executePost(playgroundHttpRequest); - if (playgroundResponse.getStatusCode() == 200) { - String playgroundCode = JSONObject.parseObject(playgroundResponse.getBody()).getJSONObject("data").getJSONArray("allPlaygroundCodes").getJSONObject(0).getString("code"); - article = article.replace(group, "```\n" + playgroundCode + "\n```"); - } - } - - Matcher imageGroupMatcher = imageGroupPattern.matcher(content); - while (imageGroupMatcher.find()) { - String group = imageGroupMatcher.group(); - HttpRequest imageGroupHttpRequest = HttpRequest.get(URLUtils.getLeetcodeProblems()+group.replace("!?!..","").replaceAll(":\\d+,\\d+!\\?!","")); - HttpResponse imageGroupResponse = HttpRequestUtils.executeGet(imageGroupHttpRequest); - if (imageGroupResponse.getStatusCode() == 200) { - JSONArray jsonArray = JSON.parseObject(imageGroupResponse.getBody()).getJSONArray("timeline"); - StringBuffer imgs = new StringBuffer(); - for (int i = 0; i < jsonArray.size(); i++) { - JSONObject jsonObject = jsonArray.getJSONObject(i); - imgs.append("![").append(i).append(" ](").append(URLUtils.getLeetcodeProblems()).append(jsonObject.getString("image").replace("..","")).append(" ) "); - } - article = article.replace(group, imgs.toString()); - } - - } - - - - return article; + public static String formatMarkdown(String content, Project project,String host) { + return CleanMarkdown.cleanMarkdown(content, host); } public static List getSolutionList(Question question, Project project) { diff --git a/src/main/java/com/shuzijun/leetcode/plugin/manager/CodeManager.java b/src/main/java/com/shuzijun/leetcode/plugin/manager/CodeManager.java index 68a81436..fd650871 100644 --- a/src/main/java/com/shuzijun/leetcode/plugin/manager/CodeManager.java +++ b/src/main/java/com/shuzijun/leetcode/plugin/manager/CodeManager.java @@ -46,8 +46,7 @@ public static void openCode(Question question, Project project) { if (file.exists()) { FileUtils.openFileEditorAndSaveState(file,project,question,fillPath,true); } else { - - if (getQuestion(question, codeTypeEnum, project)) { + if (config.getQuestionEditor() || getQuestion(question, codeTypeEnum, project)) { question.setContent(CommentUtils.createComment(question.getContent(), codeTypeEnum,config)); FileUtils.saveFile(file, VelocityUtils.convert(config.getCustomTemplate(), question)); FileUtils.openFileEditorAndSaveState(file,project,question,fillPath,true); @@ -298,21 +297,24 @@ private static boolean fillQuestion(Question question, Project project) { private static String getContent(JSONObject jsonObject) { StringBuffer sb = new StringBuffer(); sb.append(jsonObject.getString(URLUtils.getDescContent())); - JSONArray topicTagsArray = jsonObject.getJSONArray("topicTags"); - if (topicTagsArray != null && !topicTagsArray.isEmpty()) { - sb.append("
Related Topics
"); - for (int i = 0; i < topicTagsArray.size(); i++) { - JSONObject tag = topicTagsArray.getJSONObject(i); - sb.append("
  • "); - if (StringUtils.isBlank(tag.getString("translatedName"))) { - sb.append(tag.getString("name")); - } else { - sb.append(tag.getString("translatedName")); + Config config = PersistentConfig.getInstance().getConfig(); + if(config.getShowTopics()) { + JSONArray topicTagsArray = jsonObject.getJSONArray("topicTags"); + if (topicTagsArray != null && !topicTagsArray.isEmpty()) { + sb.append("
    Related Topics
    "); + for (int i = 0; i < topicTagsArray.size(); i++) { + JSONObject tag = topicTagsArray.getJSONObject(i); + sb.append("
  • "); + if (StringUtils.isBlank(tag.getString("translatedName"))) { + sb.append(tag.getString("name")); + } else { + sb.append(tag.getString("translatedName")); + } + sb.append("
  • "); } - sb.append(""); + sb.append("
    "); + sb.append("
    "); } - sb.append(""); - sb.append("
    "); } sb.append("
  • \uD83D\uDC4D "+jsonObject.getInteger("likes")+"
  • \uD83D\uDC4E "+jsonObject.getInteger("dislikes")+"
  • "); return sb.toString(); diff --git a/src/main/java/com/shuzijun/leetcode/plugin/model/Config.java b/src/main/java/com/shuzijun/leetcode/plugin/model/Config.java index bb69e198..5c818a60 100644 --- a/src/main/java/com/shuzijun/leetcode/plugin/model/Config.java +++ b/src/main/java/com/shuzijun/leetcode/plugin/model/Config.java @@ -3,10 +3,8 @@ import com.intellij.util.xmlb.annotations.Transient; import java.awt.*; -import java.util.ArrayList; -import java.util.HashMap; +import java.util.*; import java.util.List; -import java.util.Map; /** * @author shuzijun @@ -105,6 +103,16 @@ public class Config { */ private Boolean htmlContent = false; + /** + * showTopics + */ + private Boolean showTopics = true; + + /** + * showToolIcon + */ + private Boolean showToolIcon = true; + private List favoriteList; public String getId() { @@ -347,33 +355,53 @@ public void setHtmlContent(Boolean htmlContent) { this.htmlContent = htmlContent; } + public Boolean getShowTopics() { + return showTopics; + } + + public void setShowTopics(Boolean showTopics) { + this.showTopics = showTopics; + } + + public Boolean getShowToolIcon() { + return showToolIcon; + } + + public void setShowToolIcon(Boolean showToolIcon) { + this.showToolIcon = showToolIcon; + } + public boolean isModified(Config config){ if(config ==null){ return false; } - if (version != null ? !version.equals(config.version) : config.version != null) return false; - if (loginName != null ? !loginName.equals(config.loginName) : config.loginName != null) return false; - if (filePath != null ? !filePath.equals(config.filePath) : config.filePath != null) return false; - if (codeType != null ? !codeType.equals(config.codeType) : config.codeType != null) return false; - if (url != null ? !url.equals(config.url) : config.url != null) return false; - if (update != null ? !update.equals(config.update) : config.update != null) return false; - if (proxy != null ? !proxy.equals(config.proxy) : config.proxy != null) return false; - if (customCode != null ? !customCode.equals(config.customCode) : config.customCode != null) return false; - if (englishContent != null ? !englishContent.equals(config.englishContent) : config.englishContent != null) + if (!Objects.equals(version, config.version)) return false; + if (!Objects.equals(loginName, config.loginName)) return false; + if (!Objects.equals(filePath, config.filePath)) return false; + if (!Objects.equals(codeType, config.codeType)) return false; + if (!Objects.equals(url, config.url)) return false; + if (!Objects.equals(update, config.update)) return false; + if (!Objects.equals(proxy, config.proxy)) return false; + if (!Objects.equals(customCode, config.customCode)) return false; + if (!Objects.equals(englishContent, config.englishContent)) + return false; + if (!Objects.equals(customFileName, config.customFileName)) + return false; + if (!Objects.equals(customTemplate, config.customTemplate)) return false; - if (customFileName != null ? !customFileName.equals(config.customFileName) : config.customFileName != null) + if (!Objects.equals(jcef, config.jcef)) return false; - if (customTemplate != null ? !customTemplate.equals(config.customTemplate) : config.customTemplate != null) + if (!Objects.equals(questionEditor, config.questionEditor)) return false; - if (jcef != null ? !jcef.equals(config.jcef) : config.jcef != null) + if (!Objects.equals(multilineComment, config.multilineComment)) return false; - if (questionEditor != null ? !questionEditor.equals(config.questionEditor) : config.questionEditor != null) + if (!Objects.equals(htmlContent, config.htmlContent)) return false; - if (multilineComment != null ? !multilineComment.equals(config.multilineComment) : config.multilineComment != null) + if (!Objects.equals(showTopics, config.showTopics)) return false; - if (htmlContent != null ? !htmlContent.equals(config.htmlContent) : config.htmlContent != null) + if (!Objects.equals(showToolIcon, config.showToolIcon)) return false; - return levelColour != null ? levelColour.equals(config.levelColour) : config.levelColour == null; + return Objects.equals(levelColour, config.levelColour); } diff --git a/src/main/java/com/shuzijun/leetcode/plugin/model/PluginConstant.java b/src/main/java/com/shuzijun/leetcode/plugin/model/PluginConstant.java index 40375f60..68dd51a5 100644 --- a/src/main/java/com/shuzijun/leetcode/plugin/model/PluginConstant.java +++ b/src/main/java/com/shuzijun/leetcode/plugin/model/PluginConstant.java @@ -56,4 +56,7 @@ public class PluginConstant { public static final String LEETCODE_EDITOR_GROUP = ACTION_PREFIX + ".editor.group"; public static final String LEETCODE_EDITOR_TIMER_STATUS_BAR_ID = PLUGIN_ID + "-TimerStatusBar"; + + public static final String LEETCODE_EDITOR_VIEW = "lcv"; + } diff --git a/src/main/java/com/shuzijun/leetcode/plugin/setting/SettingUI.form b/src/main/java/com/shuzijun/leetcode/plugin/setting/SettingUI.form index 4d342bcb..c991bf55 100644 --- a/src/main/java/com/shuzijun/leetcode/plugin/setting/SettingUI.form +++ b/src/main/java/com/shuzijun/leetcode/plugin/setting/SettingUI.form @@ -13,7 +13,7 @@ - + @@ -157,6 +157,24 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/com/shuzijun/leetcode/plugin/setting/SettingUI.java b/src/main/java/com/shuzijun/leetcode/plugin/setting/SettingUI.java index ab471f45..df58a53e 100644 --- a/src/main/java/com/shuzijun/leetcode/plugin/setting/SettingUI.java +++ b/src/main/java/com/shuzijun/leetcode/plugin/setting/SettingUI.java @@ -59,6 +59,8 @@ public class SettingUI { private JCheckBox jcefCheckBox; private JCheckBox multilineCheckBox; private JCheckBox htmlContentCheckBox; + private JCheckBox showTopicsCheckBox; + private JCheckBox showToolIconCheckBox; private Editor fileNameEditor = null; @@ -190,6 +192,8 @@ private void loadSetting() { questionEditorCheckBox.setSelected(config.getQuestionEditor()); multilineCheckBox.setSelected(config.getMultilineComment()); htmlContentCheckBox.setSelected(config.getHtmlContent()); + showTopicsCheckBox.setSelected(config.getShowTopics()); + showToolIconCheckBox.setSelected(config.getShowToolIcon()); } else { Color[] colors = new Config().getFormatLevelColour(); easyLabel.setForeground(colors[0]); @@ -260,6 +264,8 @@ public void process(Config config) { config.setQuestionEditor(questionEditorCheckBox.isSelected()); config.setMultilineComment(multilineCheckBox.isSelected()); config.setHtmlContent(htmlContentCheckBox.isSelected()); + config.setShowTopics(showTopicsCheckBox.isSelected()); + config.setShowToolIcon(showToolIconCheckBox.isSelected()); } diff --git a/src/main/java/com/shuzijun/leetcode/plugin/utils/FileUtils.java b/src/main/java/com/shuzijun/leetcode/plugin/utils/FileUtils.java index 5e0203d8..e1003074 100644 --- a/src/main/java/com/shuzijun/leetcode/plugin/utils/FileUtils.java +++ b/src/main/java/com/shuzijun/leetcode/plugin/utils/FileUtils.java @@ -250,4 +250,12 @@ public static void saveEditDocument(VirtualFile file){ } } + public static String separator() { + if (File.separator.equals("\\")) { + return "/"; + } else { + return ""; + } + } + } diff --git a/src/main/java/com/shuzijun/leetcode/plugin/utils/doc/CleanExtensions.java b/src/main/java/com/shuzijun/leetcode/plugin/utils/doc/CleanExtensions.java new file mode 100644 index 00000000..e7aeb1a2 --- /dev/null +++ b/src/main/java/com/shuzijun/leetcode/plugin/utils/doc/CleanExtensions.java @@ -0,0 +1,35 @@ +package com.shuzijun.leetcode.plugin.utils.doc; + +import com.vladsch.flexmark.formatter.Formatter; +import com.vladsch.flexmark.parser.Parser; +import com.vladsch.flexmark.util.data.MutableDataHolder; + +/** + * @author shuzijun + */ +public class CleanExtensions implements Formatter.FormatterExtension, Parser.ParserExtension { + + public static CleanExtensions create() { + return new CleanExtensions(); + } + + @Override + public void rendererOptions(MutableDataHolder mutableDataHolder) { + + } + + @Override + public void extend(Formatter.Builder builder) { + builder.nodeFormatterFactory(new CleanNodeFormatter.Factory()); + } + + @Override + public void parserOptions(MutableDataHolder options) { + + } + + @Override + public void extend(Parser.Builder parserBuilder) { + parserBuilder.customDelimiterProcessor(new InlineLaTexProcessor()); + } +} diff --git a/src/main/java/com/shuzijun/leetcode/plugin/utils/doc/CleanMarkdown.java b/src/main/java/com/shuzijun/leetcode/plugin/utils/doc/CleanMarkdown.java new file mode 100644 index 00000000..b87e6ee9 --- /dev/null +++ b/src/main/java/com/shuzijun/leetcode/plugin/utils/doc/CleanMarkdown.java @@ -0,0 +1,34 @@ +package com.shuzijun.leetcode.plugin.utils.doc; + +import com.vladsch.flexmark.ext.attributes.AttributesExtension; +import com.vladsch.flexmark.formatter.Formatter; +import com.vladsch.flexmark.parser.Parser; +import com.vladsch.flexmark.util.ast.Node; +import com.vladsch.flexmark.util.data.DataHolder; +import com.vladsch.flexmark.util.data.MutableDataSet; + +import java.util.Arrays; + +/** + * @author shuzijun + */ +public class CleanMarkdown { + final private static DataHolder OPTIONS = new MutableDataSet() + .set(Parser.EXTENSIONS, Arrays.asList(CleanExtensions.create(), AttributesExtension.create())) + .set(Parser.LISTS_AUTO_LOOSE, false) + .toImmutable(); + + static final Parser PARSER = Parser.builder(OPTIONS).build(); + static final Formatter RENDERER = Formatter.builder(OPTIONS).build(); + + public static String cleanMarkdown(String markdown, String host) { + try { + CleanNodeFormatter.getThreadDataHolder().set(CleanNodeFormatter.FORMAT_HOST, host); + Node document = PARSER.parse(markdown); + return RENDERER.render(document); + } finally { + CleanNodeFormatter.removeThreadDataHolder(); + } + } + +} diff --git a/src/main/java/com/shuzijun/leetcode/plugin/utils/doc/CleanNodeFormatter.java b/src/main/java/com/shuzijun/leetcode/plugin/utils/doc/CleanNodeFormatter.java new file mode 100644 index 00000000..f7492b22 --- /dev/null +++ b/src/main/java/com/shuzijun/leetcode/plugin/utils/doc/CleanNodeFormatter.java @@ -0,0 +1,165 @@ +package com.shuzijun.leetcode.plugin.utils.doc; + +import com.intellij.util.Url; +import com.intellij.util.Urls; +import com.intellij.util.io.URLUtil; +import com.vladsch.flexmark.ast.FencedCodeBlock; +import com.vladsch.flexmark.ast.HtmlBlock; +import com.vladsch.flexmark.ast.HtmlInline; +import com.vladsch.flexmark.ast.Image; +import com.vladsch.flexmark.ext.attributes.AttributesNode; +import com.vladsch.flexmark.formatter.*; +import com.vladsch.flexmark.util.data.DataHolder; +import com.vladsch.flexmark.util.data.DataKey; +import com.vladsch.flexmark.util.data.MutableDataHolder; +import com.vladsch.flexmark.util.data.MutableDataSet; +import com.vladsch.flexmark.util.sequence.BasedSequence; +import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.parser.Parser; +import org.jsoup.select.Elements; + +import java.io.StringReader; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * @author shuzijun + */ +public class CleanNodeFormatter implements NodeFormatter { + + public static final DataKey FORMAT_HOST = new DataKey<>("FORMAT_HOST", ""); + + private static final ThreadLocal threadOptions = ThreadLocal.withInitial(() -> new MutableDataSet()); + + public static MutableDataHolder getThreadDataHolder() { + return threadOptions.get(); + } + + public static void removeThreadDataHolder() { + threadOptions.remove(); + } + + @Override + public @Nullable Set> getNodeFormattingHandlers() { + return new HashSet<>(Arrays.asList( + new NodeFormattingHandler<>(FencedCodeBlock.class, CleanNodeFormatter.this::render), + new NodeFormattingHandler<>(HtmlBlock.class, CleanNodeFormatter.this::render), + new NodeFormattingHandler<>(HtmlInline.class, CleanNodeFormatter.this::render), + new NodeFormattingHandler<>(AttributesNode.class, CleanNodeFormatter.this::render), + new NodeFormattingHandler<>(InlineLaTexNode.class, CleanNodeFormatter.this::render), + new NodeFormattingHandler<>(Image.class, CleanNodeFormatter.this::render) + )); + } + + + private void render(@NotNull InlineLaTexNode texNode, @NotNull NodeFormatterContext nodeFormatterContext, @NotNull MarkdownWriter markdown) { + if (texNode.getText().startsWith(texNode.getOpeningDelimiter())) { + markdown.append(texNode.getText()); + } else { + markdown.append(texNode.getOpeningDelimiter()).append(texNode.getText()).append(texNode.getClosingDelimiter()); + } + } + + private void render(@NotNull AttributesNode attributesNode, @NotNull NodeFormatterContext nodeFormatterContext, @NotNull MarkdownWriter lineInfos) { + } + + private void render(FencedCodeBlock node, NodeFormatterContext context, MarkdownWriter markdown) { + BasedSequence[] basedSequences = node.getInfo().split(" "); + if (basedSequences.length > 1 && basedSequences[1].startsWith("[")) { + node.setInfo(basedSequences[0]); + markdown.blankLine(); + markdown.append("* "); + for (int i = 1; i < basedSequences.length; i++) { + markdown.append(basedSequences[i].toString()); + } + } else { + markdown.blankLine(); + markdown.append("* ").append(node.getInfo().toString()); + } + context.delegateRender(); + } + + private void render(@NotNull HtmlBlock htmlBlock, @NotNull NodeFormatterContext context, @NotNull MarkdownWriter lineInfos) { + htmlBlock.setChars(formatHtml(htmlBlock.getChars())); + context.delegateRender(); + } + + private void render(HtmlInline node, NodeFormatterContext context, MarkdownWriter markdown) { + if (node.getChars().startsWith(" Video is not supported."); + } else if (node.getChars().startsWith(""); + } else { + context.delegateRender(); + } + + } + + private void render(Image node, NodeFormatterContext context, MarkdownWriter markdown) { + String url = formatUrl(node.getPageRef().toString()); + node.setPageRef(BasedSequence.of(url)); + context.delegateRender(); + } + + @Override + public @Nullable Set> getNodeClasses() { + return null; + } + + public static class Factory implements NodeFormatterFactory { + public Factory() { + } + + @NotNull + public NodeFormatter create(@NotNull DataHolder options) { + return new CleanNodeFormatter(); + } + } + + private BasedSequence formatHtml(BasedSequence Html) { + try { + Document document = Parser.xmlParser().parseInput(new StringReader(Html.toString()), ""); + Elements elements = document.children(); + for (Element element : elements) { + recursionElement(element); + } + return BasedSequence.of(document.html()); + } catch (Exception e) { + return Html; + } + } + + private void recursionElement(Element element) { + if ("img".equals(element.tagName())) { + String src = element.attr("src"); + if (StringUtils.isNotBlank(src)) { + element.attr("src", formatUrl(src)); + } + } else { + Elements elements = element.children(); + for (Element element1 : elements) { + recursionElement(element1); + } + } + } + + private String formatUrl(String url) { + if (!url.startsWith("http://") && !url.startsWith("https://")) { + String host = FORMAT_HOST.get(threadOptions.get()); + if (url.startsWith("/")) { + Url urlParse = Urls.parseEncoded(host); + url = urlParse.getScheme() + URLUtil.SCHEME_SEPARATOR + urlParse.getAuthority() + url; + } else { + url = host + (host.endsWith("/") ? "" : "/") + url; + } + } + url = url.replace("\\(", "("); + url = url.replace("\\)", ")"); + return url; + } +} diff --git a/src/main/java/com/shuzijun/leetcode/plugin/utils/doc/InlineLaTexNode.java b/src/main/java/com/shuzijun/leetcode/plugin/utils/doc/InlineLaTexNode.java new file mode 100644 index 00000000..c99702e7 --- /dev/null +++ b/src/main/java/com/shuzijun/leetcode/plugin/utils/doc/InlineLaTexNode.java @@ -0,0 +1,37 @@ +package com.shuzijun.leetcode.plugin.utils.doc; + +import com.vladsch.flexmark.ast.DelimitedNodeImpl; +import com.vladsch.flexmark.parser.core.delimiter.Delimiter; + +/** + * @author shuzijun + */ +public class InlineLaTexNode extends DelimitedNodeImpl { + + public static final String HTML_TEX = "LaTex"; + + public static final String TOKEN_OPEN = "$"; + public static final String TOKEN_CLOSE = "$"; + + private final String mOpener; + private final String mCloser; + + public InlineLaTexNode(final Delimiter opener, final Delimiter closer) { + mOpener = getDelimiter(opener); + mCloser = getDelimiter(closer); + } + + public String getOpeningDelimiter() { + return mOpener; + } + + public String getClosingDelimiter() { + return mCloser; + } + + private String getDelimiter(final Delimiter delimiter) { + return delimiter.getInput().subSequence( + delimiter.getStartIndex(), delimiter.getEndIndex() + ).toString(); + } +} diff --git a/src/main/java/com/shuzijun/leetcode/plugin/utils/doc/InlineLaTexProcessor.java b/src/main/java/com/shuzijun/leetcode/plugin/utils/doc/InlineLaTexProcessor.java new file mode 100644 index 00000000..824f9542 --- /dev/null +++ b/src/main/java/com/shuzijun/leetcode/plugin/utils/doc/InlineLaTexProcessor.java @@ -0,0 +1,76 @@ +package com.shuzijun.leetcode.plugin.utils.doc; + +import com.vladsch.flexmark.parser.InlineParser; +import com.vladsch.flexmark.parser.core.delimiter.Delimiter; +import com.vladsch.flexmark.parser.delimiter.DelimiterProcessor; +import com.vladsch.flexmark.parser.delimiter.DelimiterRun; +import com.vladsch.flexmark.util.ast.Node; + +/** + * @author shuzijun + */ +public class InlineLaTexProcessor implements DelimiterProcessor { + + @Override + public void process(final Delimiter opener, + final Delimiter closer, + final int delimitersUsed) { + final InlineLaTexNode node = new InlineLaTexNode(opener, closer); + opener.moveNodesBetweenDelimitersTo(node, closer); + } + + @Override + public char getOpeningCharacter() { + return '$'; + } + + @Override + public char getClosingCharacter() { + return '$'; + } + + @Override + public int getMinLength() { + return 1; + } + + @Override + public int getDelimiterUse(final DelimiterRun opener, final DelimiterRun closer) { + return 1; + } + + @Override + public boolean canBeOpener(final String before, + final String after, + final boolean leftFlanking, + final boolean rightFlanking, + final boolean beforeIsPunctuation, + final boolean afterIsPunctuation, + final boolean beforeIsWhitespace, + final boolean afterIsWhiteSpace) { + return leftFlanking; + } + + @Override + public boolean canBeCloser(final String before, + final String after, + final boolean leftFlanking, + final boolean rightFlanking, + final boolean beforeIsPunctuation, + final boolean afterIsPunctuation, + final boolean beforeIsWhitespace, + final boolean afterIsWhiteSpace) { + return rightFlanking; + } + + @Override + public Node unmatchedDelimiterNode( + final InlineParser inlineParser, final DelimiterRun delimiter) { + return null; + } + + @Override + public boolean skipNonOpenerCloser() { + return false; + } +} \ No newline at end of file diff --git a/src/main/java/com/shuzijun/leetcode/plugin/window/JcefLogin.java b/src/main/java/com/shuzijun/leetcode/plugin/window/JcefLogin.java index 0ec06d8a..279dba32 100644 --- a/src/main/java/com/shuzijun/leetcode/plugin/window/JcefLogin.java +++ b/src/main/java/com/shuzijun/leetcode/plugin/window/JcefLogin.java @@ -9,6 +9,7 @@ import com.shuzijun.leetcode.plugin.model.PluginConstant; import com.shuzijun.leetcode.plugin.utils.HttpRequestUtils; import com.shuzijun.leetcode.plugin.utils.LogUtils; +import com.shuzijun.leetcode.plugin.utils.PropertiesUtils; import com.shuzijun.leetcode.plugin.utils.URLUtils; import org.cef.browser.CefBrowser; import org.cef.browser.CefFrame; @@ -135,7 +136,7 @@ public boolean visit(CefCookie cefCookie, int count, int total, BoolRef boolRef) HttpRequestUtils.setCookie(cookieList); if (HttpRequestUtils.isLogin()) { HttpLogin.loginSuccess(tree, project, cookieList); - browser.executeJavaScript("alert('Login is successful. close the window')", "leetcode-editor", 0); + browser.executeJavaScript("alert('"+ PropertiesUtils.getInfo("browser.login.success") +"')", "leetcode-editor", 0); successDispose = true; } else { cookieList.clear(); diff --git a/src/main/java/com/shuzijun/leetcode/plugin/window/WindowFactory.java b/src/main/java/com/shuzijun/leetcode/plugin/window/WindowFactory.java index a60ebec4..f63c0bb1 100644 --- a/src/main/java/com/shuzijun/leetcode/plugin/window/WindowFactory.java +++ b/src/main/java/com/shuzijun/leetcode/plugin/window/WindowFactory.java @@ -11,6 +11,8 @@ import com.intellij.ui.content.ContentFactory; import com.shuzijun.leetcode.plugin.listener.UpdatePluginListener; import com.shuzijun.leetcode.plugin.model.PluginConstant; +import com.shuzijun.leetcode.plugin.setting.PersistentConfig; +import icons.LeetCodeEditorIcons; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; @@ -32,7 +34,9 @@ public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindo navigatorPanel.addAncestorListener(new UpdatePluginListener()); Content content = contentFactory.createContent(navigatorPanel, "", false); toolWindow.getContentManager().addContent(content); - + if(PersistentConfig.getInstance().getInitConfig()!=null && !PersistentConfig.getInstance().getInitConfig().getShowToolIcon()){ + toolWindow.setIcon(LeetCodeEditorIcons.EMPEROR_NEW_CLOTHES); + } } public static DataContext getDataContext(@NotNull Project project) { diff --git a/src/main/java/icons/LeetCodeEditorIcons.java b/src/main/java/icons/LeetCodeEditorIcons.java index 386be267..1e9f177f 100644 --- a/src/main/java/icons/LeetCodeEditorIcons.java +++ b/src/main/java/icons/LeetCodeEditorIcons.java @@ -9,32 +9,36 @@ */ public interface LeetCodeEditorIcons { - Icon LEETCODE_TOOL_WINDOW = IconLoader.getIcon("/icons/LeetCode.png"); + Icon LEETCODE_TOOL_WINDOW = IconLoader.getIcon("/icons/LeetCode.svg"); + Icon EMPEROR_NEW_CLOTHES = IconLoader.getIcon("/icons/emperor_new_clothes.svg"); + + Icon CLEAN = IconLoader.getIcon("/icons/clean.svg"); + Icon CLEAR = IconLoader.getIcon("/icons/clear.svg"); + Icon COLLAPSE = IconLoader.getIcon("/icons/collapse.svg"); + Icon CONFIG = IconLoader.getIcon("/icons/config.svg"); + Icon DESC = IconLoader.getIcon("/icons/desc.svg"); + Icon EDIT_DOC = IconLoader.getIcon("/icons/edit_doc.svg"); + Icon FAVORITE = IconLoader.getIcon("/icons/favorite.svg"); + Icon FILTER = IconLoader.getIcon("/icons/filter.svg"); + Icon FIND = IconLoader.getIcon("/icons/find.svg"); + Icon HELP = IconLoader.getIcon("/icons/help.svg"); + Icon HISTORY = IconLoader.getIcon("/icons/history.svg"); + Icon LOGIN = IconLoader.getIcon("/icons/login.svg"); + Icon LOGOUT = IconLoader.getIcon("/icons/logout.svg"); + Icon POPUP = IconLoader.getIcon("/icons/popup.svg"); + Icon POSITION = IconLoader.getIcon("/icons/position.svg"); + Icon PROGRESS = IconLoader.getIcon("/icons/progress.svg"); + Icon QUESTION = IconLoader.getIcon("/icons/question.svg"); + Icon RANDOM = IconLoader.getIcon("/icons/random.svg"); + Icon REFRESH = IconLoader.getIcon("/icons/refresh.svg"); + Icon RUN = IconLoader.getIcon("/icons/run.svg"); + Icon SOLUTION = IconLoader.getIcon("/icons/solution.svg"); + Icon SUBMIT = IconLoader.getIcon("/icons/submit.svg"); + Icon TIME = IconLoader.getIcon("/icons/time.svg"); + Icon SORT_ASC = IconLoader.getIcon("/icons/sortAsc.svg"); + Icon SORT_DESC = IconLoader.getIcon("/icons/sortDesc.svg"); + Icon NOTE = IconLoader.getIcon("/icons/note.svg"); + Icon LCV = IconLoader.getIcon("/icons/lcv.svg"); + Icon DONATE = IconLoader.getIcon("/icons/donate.svg"); - Icon CLEAN = IconLoader.getIcon("/icons/clean.png"); - Icon CLEAR = IconLoader.getIcon("/icons/clear.png"); - Icon COLLAPSE = IconLoader.getIcon("/icons/collapse.png"); - Icon CONFIG = IconLoader.getIcon("/icons/config.png"); - Icon DESC = IconLoader.getIcon("/icons/desc.png"); - Icon EDIT_DOC = IconLoader.getIcon("/icons/edit_doc.png"); - Icon FAVORITE = IconLoader.getIcon("/icons/favorite.png"); - Icon FILTER = IconLoader.getIcon("/icons/filter.png"); - Icon FIND = IconLoader.getIcon("/icons/find.png"); - Icon HELP = IconLoader.getIcon("/icons/help.png"); - Icon HISTORY = IconLoader.getIcon("/icons/history.png"); - Icon LOGIN = IconLoader.getIcon("/icons/login.png"); - Icon LOGOUT = IconLoader.getIcon("/icons/logout.png"); - Icon POPUP = IconLoader.getIcon("/icons/popup.png"); - Icon POSITION = IconLoader.getIcon("/icons/position.png"); - Icon PROGRESS = IconLoader.getIcon("/icons/progress.png"); - Icon QUESTION = IconLoader.getIcon("/icons/question.png"); - Icon RANDOM = IconLoader.getIcon("/icons/random.png"); - Icon REFRESH = IconLoader.getIcon("/icons/refresh.png"); - Icon RUN = IconLoader.getIcon("/icons/run.png"); - Icon SOLUTION = IconLoader.getIcon("/icons/solution.png"); - Icon SUBMIT = IconLoader.getIcon("/icons/submit.png"); - Icon TIME = IconLoader.getIcon("/icons/time.png"); - Icon SORT_ASC = IconLoader.getIcon("/icons/sortAsc.png"); - Icon SORT_DESC = IconLoader.getIcon("/icons/sortDesc.png"); - Icon NOTE = IconLoader.getIcon("/icons/note.png"); } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 480265fa..76982d84 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -1,7 +1,7 @@ leetcode-editor - leetcode editor - 6.9 + LeetCode Editor + 7.0 shuzijun com.intellij.modules.lang - org.intellij.plugins.markdown         @@ -144,7 +143,7 @@ serviceImplementation="com.shuzijun.leetcode.plugin.setting.PersistentConfig"/> - @@ -163,6 +162,10 @@ + + + @@ -247,6 +250,9 @@ + + diff --git a/src/main/resources/META-INF/pluginIcon.svg b/src/main/resources/META-INF/pluginIcon.svg new file mode 100644 index 00000000..2f576ee8 --- /dev/null +++ b/src/main/resources/META-INF/pluginIcon.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + diff --git a/src/main/resources/i18n/info.properties b/src/main/resources/i18n/info.properties index 33d34e7d..18ad83e7 100644 --- a/src/main/resources/i18n/info.properties +++ b/src/main/resources/i18n/info.properties @@ -37,4 +37,6 @@ user.email=YOU HAVE NOT VERIFIED YOUR ACCOUNT\nYou cannot submit your code to th template.variable=${0}question.title{1}\tquestion title\tex:Two Sum\n${0}question.titleSlug{1}\tquestion title slug \tex:two-sum\n${0}question.frontendQuestionId{1}\tquestion serial number\n${0}question.content{1}\tquestion content\n${0}question.code{1}\tquestion code\n$!velocityTool.camelCaseName(str)\ttransform str big camel case\n$!velocityTool.smallCamelCaseName(str)\ttransform str small camel case\n$!velocityTool.snakeCaseName(str)\ttransform str snake case\n$!velocityTool.leftPadZeros(str,n)\tpad sting with zero make str length at least n.\n$!velocityTool.date()\tThe current time tree.load=Please load questions first tree.null=Questions returned are empty -tree.select=Please select a valid question in the list \ No newline at end of file +tree.select=Please select a valid question in the list +Lang=en_US +browser.login.success=Login is successful. close the window \ No newline at end of file diff --git a/src/main/resources/i18n/info_zh.properties b/src/main/resources/i18n/info_zh.properties index d8d3578a..48cef985 100644 --- a/src/main/resources/i18n/info_zh.properties +++ b/src/main/resources/i18n/info_zh.properties @@ -10,7 +10,7 @@ login.success=\u767B\u5F55\u6210\u529F login.failed=\u767B\u5F55\u5931\u8D25 login.unknown=\u65E0\u6CD5\u5224\u65AD\u767B\u9646\u72B6\u6001\uFF0C\u8BF7\u5C1D\u8BD5\u767B\u9646\u7F51\u9875\u7248\u540E\uFF0C\u518D\u6B21\u91CD\u8BD5 login.out=\u9000\u51FA\u6210\u529F -login.not=\u8bf7\u5148\u767b\u5f55\u0028\u70b9\u51fb\u767b\u5f55\u6309\u94ae\u8fdb\u884c\u767b\u5f55\u0029 +login.not=\u8BF7\u5148\u767B\u5F55(\u70B9\u51FB\u767B\u5F55\u6309\u94AE\u8FDB\u884C\u767B\u5F55) response.cache=\u8BF7\u6C42\u9898\u76EE\u51FA\u9519,\u5C06\u52A0\u8F7D\u672C\u5730\u7F13\u5B58 response.question=\u52A0\u8F7D\u9898\u76EE\u5931\u8D25 response.restrict=\u65E0\u6CD5\u52A0\u8F7D\u9898\u76EE,\u63A2\u7D22\u9898\u76EE\u90E8\u5206\u6709\u987A\u5E8F\u9650\u5236 @@ -34,7 +34,9 @@ report=\u5411\u63D2\u4EF6\u4F5C\u8005\u62A5\u544A donate.info=\u6253\u8D4F updata=leetcode editor\u65B0\u7248\u672C {0} \u53D1\u5E03,\u8BF7\u53CA\u65F6\u66F4\u65B0 user.email=\u60A8\u5C1A\u672A\u9A8C\u8BC1\u81EA\u5DF1\u7684\u5E10\u6237\n\u5728\u9A8C\u8BC1\u7535\u5B50\u90AE\u4EF6\u4E4B\u524D\uFF0C\u60A8\u65E0\u6CD5\u5C06\u4EE3\u7801\u63D0\u4EA4\u7ED9\u8BC4\u5224\u7CFB\u7EDF\u3002\n\u60A8\u53EF\u4EE5\u5728\u4E2A\u4EBA\u8D44\u6599\u9875\u9762\u4E2D\u91CD\u65B0\u53D1\u9001\u9A8C\u8BC1\u7535\u5B50\u90AE\u4EF6\u6216\u66F4\u6539\u7535\u5B50\u90AE\u4EF6\u3002 -template.variable=${0}question.title{1}\t\u9898\u76ee\u6807\u9898\t\u793a\u4f8b:\u4e24\u6570\u4e4b\u548c\n${0}question.titleSlug{1}\t\u9898\u76ee\u6807\u8bb0\t\u793a\u4f8b:two-sum\n${0}question.frontendQuestionId{1}\t\u9898\u76ee\u7f16\u53f7\n${0}question.content{1}\t\u9898\u76ee\u63cf\u8ff0\n${0}question.code{1}\t\u9898\u76ee\u4ee3\u7801\n$!velocityTool.camelCaseName(str)\t\u8f6c\u6362\u5b57\u7b26\u4e3a\u5927\u9a7c\u5cf0\u6837\u5f0f\uff08\u5f00\u5934\u5b57\u6bcd\u5927\u5199\uff09\n$!velocityTool.smallCamelCaseName(str)\t\u8f6c\u6362\u5b57\u7b26\u4e3a\u5c0f\u9a7c\u5cf0\u6837\u5f0f\uff08\u5f00\u5934\u5b57\u6bcd\u5c0f\u5199\uff09\n$!velocityTool.snakeCaseName(str)\t\u8f6c\u6362\u5b57\u7b26\u4e3a\u86c7\u5f62\u6837\u5f0f\n$!velocityTool.leftPadZeros(str,n)\t\u5728\u5b57\u7b26\u4e32\u7684\u5de6\u8fb9\u586b\u51450\uff0c\u4f7f\u5b57\u7b26\u4e32\u7684\u957f\u5ea6\u81f3\u5c11\u4e3an\n$!velocityTool.date()\t\u83b7\u53d6\u5f53\u524d\u65f6\u95f4 +template.variable=${0}question.title{1}\t\u9898\u76EE\u6807\u9898\t\u793A\u4F8B:\u4E24\u6570\u4E4B\u548C\n${0}question.titleSlug{1}\t\u9898\u76EE\u6807\u8BB0\t\u793A\u4F8B:two-sum\n${0}question.frontendQuestionId{1}\t\u9898\u76EE\u7F16\u53F7\n${0}question.content{1}\t\u9898\u76EE\u63CF\u8FF0\n${0}question.code{1}\t\u9898\u76EE\u4EE3\u7801\n$!velocityTool.camelCaseName(str)\t\u8F6C\u6362\u5B57\u7B26\u4E3A\u5927\u9A7C\u5CF0\u6837\u5F0F\uFF08\u5F00\u5934\u5B57\u6BCD\u5927\u5199\uFF09\n$!velocityTool.smallCamelCaseName(str)\t\u8F6C\u6362\u5B57\u7B26\u4E3A\u5C0F\u9A7C\u5CF0\u6837\u5F0F\uFF08\u5F00\u5934\u5B57\u6BCD\u5C0F\u5199\uFF09\n$!velocityTool.snakeCaseName(str)\t\u8F6C\u6362\u5B57\u7B26\u4E3A\u86C7\u5F62\u6837\u5F0F\n$!velocityTool.leftPadZeros(str,n)\t\u5728\u5B57\u7B26\u4E32\u7684\u5DE6\u8FB9\u586B\u51450\uFF0C\u4F7F\u5B57\u7B26\u4E32\u7684\u957F\u5EA6\u81F3\u5C11\u4E3An\n$!velocityTool.date()\t\u83B7\u53D6\u5F53\u524D\u65F6\u95F4 tree.load=\u8BF7\u5148\u52A0\u8F7D\u9898\u76EE tree.null=\u83B7\u53D6\u9898\u76EE\u4E3A\u7A7A -tree.select=\u8BF7\u5728\u5217\u8868\u4E2D\u9009\u62E9\u4E00\u4E2A\u6709\u6548\u7684\u9898\u76EE \ No newline at end of file +tree.select=\u8BF7\u5728\u5217\u8868\u4E2D\u9009\u62E9\u4E00\u4E2A\u6709\u6548\u7684\u9898\u76EE +Lang=zh_CN +browser.login.success=\u767B\u5F55\u6210\u529F,\u8BF7\u624B\u52A8\u5173\u95ED\u6B64\u7A97\u53E3 \ No newline at end of file diff --git a/src/main/resources/icons/donate.svg b/src/main/resources/icons/donate.svg new file mode 100644 index 00000000..12d7a55e --- /dev/null +++ b/src/main/resources/icons/donate.svg @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/src/main/resources/icons/donate_dark.svg b/src/main/resources/icons/donate_dark.svg new file mode 100644 index 00000000..94c63af4 --- /dev/null +++ b/src/main/resources/icons/donate_dark.svg @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/src/main/resources/icons/emperor_new_clothes.svg b/src/main/resources/icons/emperor_new_clothes.svg new file mode 100644 index 00000000..33dead99 --- /dev/null +++ b/src/main/resources/icons/emperor_new_clothes.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/src/main/resources/icons/lcv.svg b/src/main/resources/icons/lcv.svg new file mode 100644 index 00000000..9c06e463 --- /dev/null +++ b/src/main/resources/icons/lcv.svg @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/resources/icons/lcv_dark.svg b/src/main/resources/icons/lcv_dark.svg new file mode 100644 index 00000000..6d738432 --- /dev/null +++ b/src/main/resources/icons/lcv_dark.svg @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/src/main/resources/template/default.html b/src/main/resources/template/default.html new file mode 100644 index 00000000..486ad8a3 --- /dev/null +++ b/src/main/resources/template/default.html @@ -0,0 +1,58 @@ + + + + + + + + + +
    + + + +{{ideStyle}} + \ No newline at end of file