From 5ba9457e4a6f83cecb83f46a01d20ee6b5f1c3f1 Mon Sep 17 00:00:00 2001 From: Peter Vanusanik Date: Sun, 19 Mar 2023 10:53:24 +0100 Subject: [PATCH] #63 --- CHANGELOG.md | 3 + README.md | 5 +- build.gradle.kts | 2 +- .../slt/plugin/SltHyperspecWindowFactory.java | 29 ++ .../slt/plugin/actions/OpenCLHSAction.java | 64 +++++ .../plugin/services/SltProjectService.java | 86 ++++++ .../SltLispEnvironmentSymbolCache.java | 10 +- .../slt/plugin/ui/SltHyperspecView.java | 252 ++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 13 + .../resources/messages/SltBundle.properties | 13 +- 10 files changed, 468 insertions(+), 9 deletions(-) create mode 100644 src/main/java/com/en_circle/slt/plugin/SltHyperspecWindowFactory.java create mode 100644 src/main/java/com/en_circle/slt/plugin/actions/OpenCLHSAction.java create mode 100644 src/main/java/com/en_circle/slt/plugin/services/SltProjectService.java create mode 100644 src/main/java/com/en_circle/slt/plugin/ui/SltHyperspecView.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 5466dad..31eb1df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ ## 0.5.0 +### Added +- Hyperspec embedded in the tool window (requires internet connection to see obviously) + ### Changes - support for definitions under other expressions - SltPlainTextSymbolCompletionContributor - to be used with git and such diff --git a/README.md b/README.md index 2da403f..fd8fcc9 100644 --- a/README.md +++ b/README.md @@ -80,17 +80,18 @@ You can also open this as a project in Intellij Idea. ## Planned features / goals -* [ ] Upload to marketplace when it has enough features +* [x] Upload to marketplace when it has enough features * [x] Automatic indentation * [x] REPL * [x] Interactive debugging * [x] Argument help (Ctrl+P) * [x] Inspection * [x] Basic inspection - * [ ] Actions + * [x] Actions * [ ] Inspection eval * [x] Breakpoints * [x] Documentation + * [x] Hyperspec intergration * [x] Macro expand in documentation * Macro expand requires you to hover element twice for now * [x] Find function by symbol name diff --git a/build.gradle.kts b/build.gradle.kts index bde5499..0faf717 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -15,7 +15,7 @@ dependencies { implementation("org.watertemplate:watertemplate-engine:1.2.2") implementation("com.google.guava:guava:31.1-jre") implementation("org.rauschig:jarchivelib:1.2.0") - implementation("org.abcl:abcl:1.8.0") + implementation("org.jsoup:jsoup:1.7.2") testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.2") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.9.2") diff --git a/src/main/java/com/en_circle/slt/plugin/SltHyperspecWindowFactory.java b/src/main/java/com/en_circle/slt/plugin/SltHyperspecWindowFactory.java new file mode 100644 index 0000000..34ed838 --- /dev/null +++ b/src/main/java/com/en_circle/slt/plugin/SltHyperspecWindowFactory.java @@ -0,0 +1,29 @@ +package com.en_circle.slt.plugin; + +import com.en_circle.slt.plugin.ui.SltHyperspecView; +import com.en_circle.slt.plugin.ui.SltUIService; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.Disposer; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowFactory; +import com.intellij.ui.content.Content; +import com.intellij.ui.content.ContentFactory; +import org.jetbrains.annotations.NotNull; + +public class SltHyperspecWindowFactory implements ToolWindowFactory, DumbAware { + + @Override + public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) { + { + SltHyperspecView hyperspecView = new SltHyperspecView(toolWindow); + ContentFactory contentFactory = ContentFactory.getInstance(); + Content content = contentFactory.createContent(hyperspecView.getContent(), + SltBundle.message("slt.ui.clhs.title"), false); + toolWindow.getContentManager().addContent(content); + + Disposer.register(SltUIService.getInstance(project), hyperspecView); + } + } + +} diff --git a/src/main/java/com/en_circle/slt/plugin/actions/OpenCLHSAction.java b/src/main/java/com/en_circle/slt/plugin/actions/OpenCLHSAction.java new file mode 100644 index 0000000..def8404 --- /dev/null +++ b/src/main/java/com/en_circle/slt/plugin/actions/OpenCLHSAction.java @@ -0,0 +1,64 @@ +package com.en_circle.slt.plugin.actions; + +import com.en_circle.slt.plugin.SltCommonLispFileType; +import com.en_circle.slt.plugin.lisp.psi.LispSymbol; +import com.en_circle.slt.plugin.services.SltProjectService; +import com.intellij.openapi.actionSystem.ActionUpdateThread; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.CommonDataKeys; +import com.intellij.openapi.editor.Editor; +import com.intellij.psi.PsiDocumentManager; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiFile; +import com.intellij.psi.util.PsiTreeUtil; +import org.jetbrains.annotations.NotNull; + +import java.util.Objects; + +public class OpenCLHSAction extends AnAction { + + @Override + public void actionPerformed(@NotNull AnActionEvent event) { + Editor editor = event.getData(CommonDataKeys.EDITOR); + if (editor != null && event.getProject() != null) { + PsiFile file = PsiDocumentManager.getInstance(Objects.requireNonNull(editor.getProject())).getPsiFile(editor.getDocument()); + if (file != null && SltCommonLispFileType.INSTANCE.equals(file.getFileType())) { + PsiElement element = file.findElementAt(editor.getCaretModel().getOffset()); + LispSymbol symbol = null; + if (element instanceof LispSymbol lispSymbol) { + symbol = lispSymbol; + } else { + symbol = PsiTreeUtil.getParentOfType(element, LispSymbol.class); + if (symbol == null) { + symbol = PsiTreeUtil.getChildOfType(element, LispSymbol.class); + } + } + + if (symbol != null) { + SltProjectService.getInstance(editor.getProject()) + .showCLHSSymbol(Objects.requireNonNull(symbol.getName()).toLowerCase()); + } + } + } + } + + @Override + public void update(@NotNull AnActionEvent event) { + event.getPresentation().setEnabledAndVisible(false); + + Editor editor = event.getData(CommonDataKeys.EDITOR); + if (editor != null && event.getProject() != null) { + PsiFile file = PsiDocumentManager.getInstance(Objects.requireNonNull(editor.getProject())).getPsiFile(editor.getDocument()); + if (file != null && SltCommonLispFileType.INSTANCE.equals(file.getFileType())) { + event.getPresentation().setEnabledAndVisible(true); + } + } + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.BGT; + } + +} diff --git a/src/main/java/com/en_circle/slt/plugin/services/SltProjectService.java b/src/main/java/com/en_circle/slt/plugin/services/SltProjectService.java new file mode 100644 index 0000000..81e1b91 --- /dev/null +++ b/src/main/java/com/en_circle/slt/plugin/services/SltProjectService.java @@ -0,0 +1,86 @@ +package com.en_circle.slt.plugin.services; + +import com.en_circle.slt.plugin.SltBundle; +import com.en_circle.slt.plugin.ui.SltHyperspecView; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.Messages; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowManager; +import org.apache.commons.io.IOUtils; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.URL; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; + +public class SltProjectService implements DumbAware { + private static final Logger log = LoggerFactory.getLogger(SltProjectService.class); + + public static SltProjectService getInstance(Project project) { + return project.getService(SltProjectService.class); + } + + private SltHyperspecView hyperspecView; + private Map symbolRefMap = null; + private final Project project; + + public SltProjectService(Project project) { + this.project = project; + ApplicationManager.getApplication().executeOnPooledThread(this::loadHyperspec); + } + + private void loadHyperspec() { + try { + Map symbolParseMap = new HashMap<>(); + + String base = "http://www.lispworks.com/documentation/HyperSpec/Front/"; + String html = IOUtils.toString(new URL("http://www.lispworks.com/documentation/HyperSpec/Front/X_AllSym.htm"), + Charset.defaultCharset()); + Document document = Jsoup.parse(html); + Elements links = document.select("a[href]"); + for (Element link : links) { + if ("DEFINITION".equals(link.attr("REL"))) { + String ref = link.text(); + String page = base + link.attr("HREF"); + symbolParseMap.put(ref, page); + } + } + + symbolRefMap = symbolParseMap; + } catch (Exception e) { + log.error(e.getMessage()); + } + } + + public void showCLHSSymbol(String symbolName) { + if (symbolRefMap != null) { + String url = symbolRefMap.get(symbolName); + if (url != null) { + ToolWindow toolWindow = ToolWindowManager.getInstance(project) + .getToolWindow("CLHS"); + assert toolWindow != null; + toolWindow.show(() -> { + if (hyperspecView != null) { + hyperspecView.showUrl(url); + } + }); + return; + } + } + + Messages.showInfoMessage(String.format(SltBundle.message("slt.ui.clhs.nosymbol.message"), symbolName), + SltBundle.message("slt.ui.clhs.nosymbol.title")); + } + + public void setHyperspecView(SltHyperspecView hyperspecView) { + this.hyperspecView = hyperspecView; + } +} diff --git a/src/main/java/com/en_circle/slt/plugin/services/lisp/components/SltLispEnvironmentSymbolCache.java b/src/main/java/com/en_circle/slt/plugin/services/lisp/components/SltLispEnvironmentSymbolCache.java index 7270c6d..fbd19cd 100644 --- a/src/main/java/com/en_circle/slt/plugin/services/lisp/components/SltLispEnvironmentSymbolCache.java +++ b/src/main/java/com/en_circle/slt/plugin/services/lisp/components/SltLispEnvironmentSymbolCache.java @@ -10,12 +10,12 @@ import com.en_circle.slt.tools.SltApplicationUtils; import com.google.common.collect.Lists; import com.intellij.openapi.project.Project; -import com.intellij.util.Consumer; import com.intellij.util.concurrency.FutureResult; import org.apache.commons.lang3.StringUtils; import java.util.*; import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -96,10 +96,10 @@ private void refreshBatchedSymbols(BatchedSymbolRefreshAction action, Consumer refreshStates, Consumer { if (requestFinished != null) { - requestFinished.consume(false); + requestFinished.accept(false); } }); } diff --git a/src/main/java/com/en_circle/slt/plugin/ui/SltHyperspecView.java b/src/main/java/com/en_circle/slt/plugin/ui/SltHyperspecView.java new file mode 100644 index 0000000..402a323 --- /dev/null +++ b/src/main/java/com/en_circle/slt/plugin/ui/SltHyperspecView.java @@ -0,0 +1,252 @@ +package com.en_circle.slt.plugin.ui; + +import com.en_circle.slt.plugin.SltBundle; +import com.en_circle.slt.plugin.services.SltProjectService; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.Disposable; +import com.intellij.openapi.actionSystem.*; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.ui.components.JBTextField; +import com.intellij.ui.jcef.JBCefBrowser; +import com.intellij.ui.jcef.JBCefClient; +import org.cef.browser.CefBrowser; +import org.cef.handler.CefLoadHandlerAdapter; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ContainerAdapter; +import java.awt.event.ContainerEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +public class SltHyperspecView implements Disposable { + + private static final String BASE_URL = "https://www.lispworks.com/documentation/HyperSpec/"; + + private final ToolWindow toolWindow; + private final Project project; + private final JPanel panel; + private final JBCefBrowser browser; + private final JBCefClient client; + private final JBTextField address; + private boolean hidden = true; + + private boolean loading = false; + + public SltHyperspecView(ToolWindow toolWindow) { + this.toolWindow = toolWindow; + this.project = toolWindow.getProject(); + + panel = new JPanel(new BorderLayout()); + panel.addContainerListener(new ContainerAdapter() { + @Override + public void componentAdded(ContainerEvent e) { + if (hidden) { + showRoot(); + } + hidden = false; + } + + @Override + public void componentRemoved(ContainerEvent e) { + hidden = true; + } + }); + browser = new JBCefBrowser(); + client = browser.getJBCefClient(); + client.addLoadHandler(new CefLoadHandlerAdapter() { + @Override + public void onLoadingStateChange(CefBrowser browser, boolean isLoading, boolean canGoBack, boolean canGoForward) { + loading = isLoading; + address.setText(browser.getURL()); + } + }, browser.getCefBrowser()); + + panel.add(browser.getComponent(), BorderLayout.CENTER); + + DefaultActionGroup controlGroup = new DefaultActionGroup(); + controlGroup.add(new GoBackAction()); + controlGroup.add(new GoForwardAction()); + controlGroup.addSeparator(); + controlGroup.add(new StopAction()); + controlGroup.add(new RefreshAction()); + controlGroup.addSeparator(); + controlGroup.add(new HomeAction()); + + JPanel header = new JPanel(new BorderLayout()); + ActionToolbar toolbar = ActionManager.getInstance() + .createActionToolbar("SltHyperspecToolbar", controlGroup, true); + toolbar.setTargetComponent(header); + header.add(toolbar.getComponent(), BorderLayout.NORTH); + + address = new JBTextField(); + address.setEditable(false); + address.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + address.setSelectionStart(0); + address.setSelectionEnd(address.getText().length()); + } + }); + header.add(address, BorderLayout.SOUTH); + + panel.add(header, BorderLayout.NORTH); + + SltProjectService.getInstance(project).setHyperspecView(this); + } + + @Override + public void dispose() { + browser.dispose(); + } + + public JComponent getContent() { + return panel; + } + + private void showRoot() { + browser.loadURL(BASE_URL); + } + + private void stop() { + browser.getCefBrowser().stopLoad(); + } + + private void refresh() { + browser.getCefBrowser().reloadIgnoreCache(); + } + + private void goBack() { + browser.getCefBrowser().goBack(); + } + + private void goForward() { + browser.getCefBrowser().goForward(); + } + + public void showUrl(String url) { + browser.loadURL(url); + } + + private class GoBackAction extends AnAction { + + private GoBackAction() { + super(SltBundle.message("slt.ui.clhs.back"), "", AllIcons.Actions.Back); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + goBack(); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.EDT; + } + + @Override + public void update(@NotNull AnActionEvent e) { + super.update(e); + + e.getPresentation().setEnabled(browser.getCefBrowser().canGoBack()); + } + } + + private class GoForwardAction extends AnAction { + + private GoForwardAction() { + super(SltBundle.message("slt.ui.clhs.forward"), "", AllIcons.Actions.Forward); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + goForward(); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.EDT; + } + + @Override + public void update(@NotNull AnActionEvent e) { + super.update(e); + + e.getPresentation().setEnabled(browser.getCefBrowser().canGoForward()); + } + } + + private class StopAction extends AnAction { + + private StopAction() { + super(SltBundle.message("slt.ui.clhs.stop"), "", AllIcons.Process.Stop); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + stop(); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.EDT; + } + + @Override + public void update(@NotNull AnActionEvent e) { + super.update(e); + + e.getPresentation().setEnabled(loading); + } + } + + private class RefreshAction extends AnAction { + + private RefreshAction() { + super(SltBundle.message("slt.ui.clhs.refresh"), "", AllIcons.Actions.Refresh); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + refresh(); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.EDT; + } + + @Override + public void update(@NotNull AnActionEvent e) { + super.update(e); + + e.getPresentation().setEnabled(!loading); + } + } + + private class HomeAction extends AnAction { + + private HomeAction() { + super(SltBundle.message("slt.ui.clhs.home"), "", AllIcons.Nodes.HomeFolder); + } + + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + showRoot(); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.EDT; + } + + @Override + public void update(@NotNull AnActionEvent e) { + super.update(e); + + e.getPresentation().setEnabled(true); + } + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 898b971..c4bfae8 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -51,6 +51,9 @@ + + + @@ -76,6 +79,9 @@ + + @@ -167,6 +173,13 @@ + + + + + diff --git a/src/main/resources/messages/SltBundle.properties b/src/main/resources/messages/SltBundle.properties index 4ec0ea9..3f44012 100644 --- a/src/main/resources/messages/SltBundle.properties +++ b/src/main/resources/messages/SltBundle.properties @@ -107,6 +107,16 @@ slt.ui.instanceinfo.threads.action.refresh=Refresh Thread List slt.ui.instanceinfo.threads.action.break=Break Thread slt.ui.instanceinfo.threads.action.stop=Terminate Thread +# Hyperspec +slt.ui.clhs.title=CLHS +slt.ui.clhs.back=Back +slt.ui.clhs.forward=Forward +slt.ui.clhs.home=Home +slt.ui.clhs.refresh=Refresh +slt.ui.clhs.stop=Stop +slt.ui.clhs.nosymbol.title=Symbol Not Found +slt.ui.clhs.nosymbol.message=Symbol '%s' not found in the CLHS + # Settings slt.ui.projectsettings.sdk=Common Lisp SDK slt.ui.projectsettings.sdk.selector=Common lisp SDK @@ -209,4 +219,5 @@ action.slt.actions.eval.previous.text=Evaluate Previous S-Expression action.slt.actions.eval.current.text=Evaluate This S-Expression action.slt.actions.eval.next.text=Evaluate Next S-Expression action.slt.actions.eval.file.editor.text=Evaluate Current File -action.slt.actions.eval.file.text=Evaluate File \ No newline at end of file +action.slt.actions.eval.file.text=Evaluate File +action.slt.actions.clhs.open.text=Show Symbol In CLHS \ No newline at end of file