From b1a8a1b23e8bd8f0b985450a6b991cfe3c601f04 Mon Sep 17 00:00:00 2001 From: Dmitry Trofimov Date: Wed, 14 Feb 2018 22:57:56 +0100 Subject: [PATCH 1/6] PyCharm professional advertiser --- python/ide/src/META-INF/PyCharmCorePlugin.xml | 2 + .../src/com/jetbrains/python/PyCellUtil.java | 74 ++++++++++ .../python/PyCharmProfessionalAdvertiser.java | 133 ++++++++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 python/src/com/jetbrains/python/PyCellUtil.java create mode 100644 python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java diff --git a/python/ide/src/META-INF/PyCharmCorePlugin.xml b/python/ide/src/META-INF/PyCharmCorePlugin.xml index c60c1bcb06df..9808080f4da0 100644 --- a/python/ide/src/META-INF/PyCharmCorePlugin.xml +++ b/python/ide/src/META-INF/PyCharmCorePlugin.xml @@ -18,5 +18,7 @@ + + diff --git a/python/src/com/jetbrains/python/PyCellUtil.java b/python/src/com/jetbrains/python/PyCellUtil.java new file mode 100644 index 000000000000..e64aa3a54daa --- /dev/null +++ b/python/src/com/jetbrains/python/PyCellUtil.java @@ -0,0 +1,74 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.jetbrains.python; + +import com.intellij.openapi.util.text.StringUtil; +import com.intellij.psi.PsiComment; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.util.PsiTreeUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public class PyCellUtil { + + public static boolean hasCells(@NotNull PsiFile file) { + PsiElement el = file.getFirstChild(); + while (el != null) { + if (isBlockCell(el)) { + return true; + } + el = el.getNextSibling(); + } + return false; + } + + @Nullable + public static PsiElement getCellStart(@NotNull PsiElement element) { + PsiElement el = element; + while (el != null && !isBlockCell(el)) { + el = PsiTreeUtil.prevLeaf(el); + } + if (el == null) { + return element.getContainingFile().getFirstChild(); + } + return PsiTreeUtil.nextLeaf(el); + } + + public static boolean isBlockCell(PsiElement element) { + return (element instanceof PsiComment) && + (element.getText().startsWith("# %%") || element.getText().startsWith("#%%") || element.getText().startsWith("# ")) || + element.getText().startsWith("# In["); + } + + @NotNull + public static String getCodeInCell(@Nullable PsiElement element) { + StringBuilder text = new StringBuilder(); + while (element != null && !isBlockCell(element)) { + text.append(element.getText()); + element = element.getNextSibling(); + } + return StringUtil.trim(text.toString()); + } + + public static PsiElement findNextCell(PsiElement startElement) { + PsiElement el = PsiTreeUtil.nextLeaf(PsiTreeUtil.getDeepestFirst(startElement)); + while (el != null) { + if (isBlockCell(el)) { + return el; + } + el = PsiTreeUtil.nextLeaf(el); + } + return null; + } + + public static PsiElement findPrevCell(PsiElement startElement) { + PsiElement el = PsiTreeUtil.prevLeaf(PsiTreeUtil.getDeepestFirst(startElement)); + while (el != null) { + if (isBlockCell(el)) { + return el; + } + el = PsiTreeUtil.prevLeaf(el); + } + return null; + } +} diff --git a/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java b/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java new file mode 100644 index 000000000000..a3860a627ef9 --- /dev/null +++ b/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java @@ -0,0 +1,133 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.jetbrains.python; + +import com.intellij.ide.BrowserUtil; +import com.intellij.lang.annotation.AnnotationHolder; +import com.intellij.lang.annotation.Annotator; +import com.intellij.notification.NotificationDisplayType; +import com.intellij.notification.NotificationGroup; +import com.intellij.notification.NotificationListener; +import com.intellij.notification.NotificationType; +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.module.Module; +import com.intellij.openapi.module.ModuleUtilCore; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.roots.FileIndexFacade; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.psi.PsiElement; +import com.intellij.psi.util.QualifiedName; +import com.jetbrains.python.psi.PyFile; +import com.jetbrains.python.psi.PyFromImportStatement; +import com.jetbrains.python.psi.PyImportElement; +import com.jetbrains.python.sdk.PythonSdkType; +import org.intellij.lang.annotations.Language; +import org.jetbrains.annotations.NotNull; + +/** + * @author traff + */ +public class PyCharmProfessionalAdvertiser implements Annotator { + + + @Language("HTML") + private static final String NOTIFICATIONS_TEXT = + "PyCharm Professional Edition has special support for it."; + + private static final NotificationGroup BALLOON_NOTIFICATIONS = new NotificationGroup("PyCharm Professional Advertiser", + NotificationDisplayType.STICKY_BALLOON, false); + private static final Key DONT_SHOW_BALLOON = Key.create("showingPyCompatibilityAdvertiserBalloon"); + + @Override + public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { + if (element instanceof PyFile) { + + final PyFile pyFile = (PyFile)element; + final Project project = element.getProject(); + + final VirtualFile vFile = pyFile.getVirtualFile(); + if (vFile != null && FileIndexFacade.getInstance(project).isInLibraryClasses(vFile)) { + return; + } + + final Boolean showingFlag = project.getUserData(DONT_SHOW_BALLOON); + if (showingFlag != null && showingFlag.booleanValue()) { + return; + } + + if (!moduleUsesPythonSdk(pyFile)) { + return; + } + + if (PyCellUtil.hasCells(pyFile)) { + showInspectionAdvertisement(project, "code cells in editor"); + } + + if (containsImport(pyFile, "django")) { + showInspectionAdvertisement(project, "Django Framework"); + } + + if (containsImport(pyFile, "flask")) { + showInspectionAdvertisement(project, "Flask Framework"); + } + + if (containsImport(pyFile, "pyramid")) { + showInspectionAdvertisement(project, "Pyramid Framework"); + } + } + } + + private static boolean moduleUsesPythonSdk(@NotNull PyFile file) { + final Module module = ModuleUtilCore.findModuleForFile(file.getVirtualFile(), file.getProject()); + if (module != null) { + return PythonSdkType.findPythonSdk(module) != null; + } + return false; + } + + private static void showInspectionAdvertisement(@NotNull Project project, @NotNull String message) { + showSingletonNotification(project, "You are using " + message, NOTIFICATIONS_TEXT, NotificationType.INFORMATION, + (notification, event) -> { + if ("prof".equals(event.getDescription())) { + BrowserUtil.browse("https://www.jetbrains.com/pycharm/features/editions_comparison_matrix.html"); + } + }); + } + + private static void showSingletonNotification(@NotNull Project project, + @NotNull String title, + @NotNull String htmlContent, + @NotNull NotificationType type, + @NotNull NotificationListener listener) { + project.putUserData(DONT_SHOW_BALLOON, true); + BALLOON_NOTIFICATIONS.createNotification(title, htmlContent, type, (notification, event) -> { + try { + listener.hyperlinkUpdate(notification, event); + } + finally { + notification.expire(); + } + }).notify(project); + } + + private static boolean containsImport(@NotNull PyFile file, String pkg) { + for (PyFromImportStatement importStatement : file.getFromImports()) { + final QualifiedName name = importStatement.getImportSourceQName(); + if (name != null && name.toString().toLowerCase().contains(pkg)) { + return true; + } + } + for (PyImportElement importElement : file.getImportTargets()) { + final QualifiedName name = importElement.getImportedQName(); + if (name != null && name.toString().toLowerCase().contains(pkg)) { + return true; + } + } + return false; + } + + @NotNull + private static PyCharmProfessionalAdvertiserSettings getSettings(@NotNull Project project) { + return ServiceManager.getService(project, PyCharmProfessionalAdvertiserSettings.class); + } +} From 6ea6af8f02327b62710c984322aa58aee7ddf663 Mon Sep 17 00:00:00 2001 From: Dmitry Trofimov Date: Thu, 15 Feb 2018 14:40:47 +0100 Subject: [PATCH 2/6] Show advertiser only once --- python/ide/src/META-INF/PyCharmCorePlugin.xml | 1 + .../python/PyCharmProfessionalAdvertiser.java | 12 +++++---- ...PyCharmProfessionalAdvertiserSettings.java | 27 +++++++++++++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 python/src/com/jetbrains/python/PyCharmProfessionalAdvertiserSettings.java diff --git a/python/ide/src/META-INF/PyCharmCorePlugin.xml b/python/ide/src/META-INF/PyCharmCorePlugin.xml index 9808080f4da0..04ca94cc22ab 100644 --- a/python/ide/src/META-INF/PyCharmCorePlugin.xml +++ b/python/ide/src/META-INF/PyCharmCorePlugin.xml @@ -20,5 +20,6 @@ overrides="true"/> + diff --git a/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java b/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java index a3860a627ef9..1221d7f97f2b 100644 --- a/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java +++ b/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java @@ -40,27 +40,29 @@ public class PyCharmProfessionalAdvertiser implements Annotator { @Override public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { + if (element instanceof PyFile) { final PyFile pyFile = (PyFile)element; final Project project = element.getProject(); + if (getSettings(project).shown) { + return; + } + getSettings(project).shown = true; + final VirtualFile vFile = pyFile.getVirtualFile(); if (vFile != null && FileIndexFacade.getInstance(project).isInLibraryClasses(vFile)) { return; } - final Boolean showingFlag = project.getUserData(DONT_SHOW_BALLOON); - if (showingFlag != null && showingFlag.booleanValue()) { - return; - } if (!moduleUsesPythonSdk(pyFile)) { return; } if (PyCellUtil.hasCells(pyFile)) { - showInspectionAdvertisement(project, "code cells in editor"); + showInspectionAdvertisement(project, "code cells in the editor"); } if (containsImport(pyFile, "django")) { diff --git a/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiserSettings.java b/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiserSettings.java new file mode 100644 index 000000000000..7f09a927adc3 --- /dev/null +++ b/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiserSettings.java @@ -0,0 +1,27 @@ +// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. +package com.jetbrains.python; + +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.State; +import com.intellij.util.xmlb.XmlSerializerUtil; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * @author traff + */ +@State(name = "PyCharmProfessionalAdvertiser") +public class PyCharmProfessionalAdvertiserSettings implements PersistentStateComponent { + public boolean shown = false; + + @Nullable + @Override + public PyCharmProfessionalAdvertiserSettings getState() { + return this; + } + + @Override + public void loadState(@NotNull PyCharmProfessionalAdvertiserSettings state) { + XmlSerializerUtil.copyBean(state, this); + } +} From 8c8a639dbb5bcfacce55056b599d78a9044b0802 Mon Sep 17 00:00:00 2001 From: Dmitry Trofimov Date: Thu, 15 Feb 2018 14:59:17 +0100 Subject: [PATCH 3/6] Change wording and add analytics markers to the url --- .../python/PyCharmProfessionalAdvertiser.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java b/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java index 1221d7f97f2b..a5735e2b91bf 100644 --- a/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java +++ b/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java @@ -62,19 +62,19 @@ public class PyCharmProfessionalAdvertiser implements Annotator { } if (PyCellUtil.hasCells(pyFile)) { - showInspectionAdvertisement(project, "code cells in the editor"); + showInspectionAdvertisement(project, "code cells in the editor", "codecells"); } if (containsImport(pyFile, "django")) { - showInspectionAdvertisement(project, "Django Framework"); + showInspectionAdvertisement(project, "the Django Framework", "django"); } if (containsImport(pyFile, "flask")) { - showInspectionAdvertisement(project, "Flask Framework"); + showInspectionAdvertisement(project, "the Flask Framework", "flask"); } if (containsImport(pyFile, "pyramid")) { - showInspectionAdvertisement(project, "Pyramid Framework"); + showInspectionAdvertisement(project, "the Pyramid Framework", "pyramid"); } } } @@ -87,11 +87,11 @@ public class PyCharmProfessionalAdvertiser implements Annotator { return false; } - private static void showInspectionAdvertisement(@NotNull Project project, @NotNull String message) { + private static void showInspectionAdvertisement(@NotNull Project project, @NotNull String message, @NotNull String source) { showSingletonNotification(project, "You are using " + message, NOTIFICATIONS_TEXT, NotificationType.INFORMATION, (notification, event) -> { if ("prof".equals(event.getDescription())) { - BrowserUtil.browse("https://www.jetbrains.com/pycharm/features/editions_comparison_matrix.html"); + BrowserUtil.browse("https://www.jetbrains.com/pycharm/features/editions_comparison_matrix.html?utm_source=from_product&utm_medium=advertiser&utm_campaign=" + source); } }); } From fa781ac9ef3b8e1feac64cad86cf78a365614b2b Mon Sep 17 00:00:00 2001 From: Dmitry Trofimov Date: Thu, 15 Feb 2018 19:13:00 +0100 Subject: [PATCH 4/6] Special landing pages for django and editor cells --- .../python/PyCharmProfessionalAdvertiser.java | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java b/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java index a5735e2b91bf..9bc7de1a0efe 100644 --- a/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java +++ b/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java @@ -13,7 +13,6 @@ import com.intellij.openapi.module.Module; import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.FileIndexFacade; -import com.intellij.openapi.util.Key; import com.intellij.openapi.vfs.VirtualFile; import com.intellij.psi.PsiElement; import com.intellij.psi.util.QualifiedName; @@ -23,6 +22,7 @@ import com.jetbrains.python.psi.PyImportElement; import com.jetbrains.python.sdk.PythonSdkType; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * @author traff @@ -36,8 +36,6 @@ public class PyCharmProfessionalAdvertiser implements Annotator { private static final NotificationGroup BALLOON_NOTIFICATIONS = new NotificationGroup("PyCharm Professional Advertiser", NotificationDisplayType.STICKY_BALLOON, false); - private static final Key DONT_SHOW_BALLOON = Key.create("showingPyCompatibilityAdvertiserBalloon"); - @Override public void annotate(@NotNull PsiElement element, @NotNull AnnotationHolder holder) { @@ -49,7 +47,6 @@ public class PyCharmProfessionalAdvertiser implements Annotator { if (getSettings(project).shown) { return; } - getSettings(project).shown = true; final VirtualFile vFile = pyFile.getVirtualFile(); if (vFile != null && FileIndexFacade.getInstance(project).isInLibraryClasses(vFile)) { @@ -62,19 +59,19 @@ public class PyCharmProfessionalAdvertiser implements Annotator { } if (PyCellUtil.hasCells(pyFile)) { - showInspectionAdvertisement(project, "code cells in the editor", "codecells"); + showInspectionAdvertisement(project, "code cells in the editor", "https://www.jetbrains.com/pycharm/features/scientific_tools.html", "codecells"); } if (containsImport(pyFile, "django")) { - showInspectionAdvertisement(project, "the Django Framework", "django"); + showInspectionAdvertisement(project, "the Django Framework", "https://www.jetbrains.com/pycharm/features/web_development.html#django","django"); } if (containsImport(pyFile, "flask")) { - showInspectionAdvertisement(project, "the Flask Framework", "flask"); + showInspectionAdvertisement(project, "the Flask Framework", null,"flask"); } if (containsImport(pyFile, "pyramid")) { - showInspectionAdvertisement(project, "the Pyramid Framework", "pyramid"); + showInspectionAdvertisement(project, "the Pyramid Framework", null,"pyramid"); } } } @@ -87,11 +84,17 @@ public class PyCharmProfessionalAdvertiser implements Annotator { return false; } - private static void showInspectionAdvertisement(@NotNull Project project, @NotNull String message, @NotNull String source) { + private static void showInspectionAdvertisement(@NotNull Project project, + @NotNull String message, + @Nullable String url, + @NotNull String source) { showSingletonNotification(project, "You are using " + message, NOTIFICATIONS_TEXT, NotificationType.INFORMATION, (notification, event) -> { if ("prof".equals(event.getDescription())) { - BrowserUtil.browse("https://www.jetbrains.com/pycharm/features/editions_comparison_matrix.html?utm_source=from_product&utm_medium=advertiser&utm_campaign=" + source); + BrowserUtil.browse( + (url != null ? url : "https://www.jetbrains.com/pycharm/features/editions_comparison_matrix.html") + + "?utm_source=from_product&utm_medium=advertiser&utm_campaign=" + + source); } }); } @@ -101,7 +104,7 @@ public class PyCharmProfessionalAdvertiser implements Annotator { @NotNull String htmlContent, @NotNull NotificationType type, @NotNull NotificationListener listener) { - project.putUserData(DONT_SHOW_BALLOON, true); + getSettings(project).shown = true; BALLOON_NOTIFICATIONS.createNotification(title, htmlContent, type, (notification, event) -> { try { listener.hyperlinkUpdate(notification, event); From 0b48aaa2be77fbff8c6dc6711f8972f09e57b410 Mon Sep 17 00:00:00 2001 From: Dmitry Trofimov Date: Thu, 15 Feb 2018 19:13:44 +0100 Subject: [PATCH 5/6] Don't check sdk --- .../com/jetbrains/python/PyCharmProfessionalAdvertiser.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java b/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java index 9bc7de1a0efe..e75497ab640c 100644 --- a/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java +++ b/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java @@ -53,11 +53,6 @@ public class PyCharmProfessionalAdvertiser implements Annotator { return; } - - if (!moduleUsesPythonSdk(pyFile)) { - return; - } - if (PyCellUtil.hasCells(pyFile)) { showInspectionAdvertisement(project, "code cells in the editor", "https://www.jetbrains.com/pycharm/features/scientific_tools.html", "codecells"); } From 04b44991327d5376d4bf794005ed5921d66eff73 Mon Sep 17 00:00:00 2001 From: Dmitry Trofimov Date: Thu, 15 Feb 2018 19:14:01 +0100 Subject: [PATCH 6/6] Clean-up --- .../python/PyCharmProfessionalAdvertiser.java | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java b/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java index e75497ab640c..1bcfc8ecafc0 100644 --- a/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java +++ b/python/src/com/jetbrains/python/PyCharmProfessionalAdvertiser.java @@ -9,8 +9,6 @@ import com.intellij.notification.NotificationGroup; import com.intellij.notification.NotificationListener; import com.intellij.notification.NotificationType; import com.intellij.openapi.components.ServiceManager; -import com.intellij.openapi.module.Module; -import com.intellij.openapi.module.ModuleUtilCore; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.FileIndexFacade; import com.intellij.openapi.vfs.VirtualFile; @@ -19,7 +17,6 @@ import com.intellij.psi.util.QualifiedName; import com.jetbrains.python.psi.PyFile; import com.jetbrains.python.psi.PyFromImportStatement; import com.jetbrains.python.psi.PyImportElement; -import com.jetbrains.python.sdk.PythonSdkType; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -71,14 +68,6 @@ public class PyCharmProfessionalAdvertiser implements Annotator { } } - private static boolean moduleUsesPythonSdk(@NotNull PyFile file) { - final Module module = ModuleUtilCore.findModuleForFile(file.getVirtualFile(), file.getProject()); - if (module != null) { - return PythonSdkType.findPythonSdk(module) != null; - } - return false; - } - private static void showInspectionAdvertisement(@NotNull Project project, @NotNull String message, @Nullable String url,