From bbc2eb447a19818d076c12db4042d6e1d957ea57 Mon Sep 17 00:00:00 2001 From: zaaarf Date: Mon, 16 Sep 2024 03:01:14 +0200 Subject: [PATCH] feat(java): brought the plugin to a broadly usable state --- .gitignore | 5 +- README.md | 3 +- build.gradle | 5 +- src/main/java/mp/code/intellij/CodeMP.java | 42 +++- .../code/intellij/actions/ConnectAction.java | 30 +-- .../intellij/actions/DisconnectAction.java | 27 +- .../workspace/WorkspaceJoinAction.java | 64 ++--- .../workspace/WorkspaceLeaveAction.java | 30 ++- .../listeners/BufferEventListener.java | 44 ++-- .../listeners/CursorEventListener.java | 43 ++-- .../WorkspaceFileClosedListener.java | 33 --- .../WorkspaceFileOpenedListener.java | 68 ------ .../intellij/task/BufferEventAwaiterTask.java | 83 ------- .../intellij/task/CursorEventAwaiterTask.java | 97 -------- .../intellij/ui/CodeMPToolWindowFactory.java | 181 ++++++++++++++ .../code/intellij/ui/SimpleMouseListener.java | 30 +++ .../mp/code/intellij/util/ActionUtil.java | 49 ---- .../java/mp/code/intellij/util/FileUtil.java | 27 ++ .../code/intellij/util/InteractionUtil.java | 231 ++++++++++++++++++ src/main/resources/META-INF/plugin.xml | 5 + 20 files changed, 632 insertions(+), 465 deletions(-) delete mode 100644 src/main/java/mp/code/intellij/listeners/WorkspaceFileClosedListener.java delete mode 100644 src/main/java/mp/code/intellij/listeners/WorkspaceFileOpenedListener.java delete mode 100644 src/main/java/mp/code/intellij/task/BufferEventAwaiterTask.java delete mode 100644 src/main/java/mp/code/intellij/task/CursorEventAwaiterTask.java create mode 100644 src/main/java/mp/code/intellij/ui/CodeMPToolWindowFactory.java create mode 100644 src/main/java/mp/code/intellij/ui/SimpleMouseListener.java delete mode 100644 src/main/java/mp/code/intellij/util/ActionUtil.java create mode 100644 src/main/java/mp/code/intellij/util/InteractionUtil.java diff --git a/.gitignore b/.gitignore index 3fd913b..39ebf41 100644 --- a/.gitignore +++ b/.gitignore @@ -47,8 +47,11 @@ Cargo.lock .cargo target -#IntelliJ test client run config +# IntelliJ test client run config .run/ +# Stuff from IntellIJ plugin +.intellijPlatform + # Do not include generated code src/main/java/com/codemp/intellij/jni diff --git a/README.md b/README.md index 2cd4cb8..e4773e3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ [![codemp](https://code.mp/static/banner.png)](https://code.mp) -> `codemp` is a **collaborative** text editing solution to work remotely. +> [`codemp`](https://github.com/hexedtech/codemp) is a **collaborative** text editing solution to work +> remotely. It seamlessly integrates in your editor providing remote cursors and instant text synchronization, as well as a remote virtual workspace for you and your team. diff --git a/build.gradle b/build.gradle index 93bb0d2..33e0061 100644 --- a/build.gradle +++ b/build.gradle @@ -60,4 +60,7 @@ tasks { } } -instrumentedJar.dependsOn shadowJar //TODO: instrumentedJar should use fatjar as input +// useful for debugging +runIde.doFirst { environment 'RUST_BACKTRACE', 'full' } + +build.dependsOn shadowJar \ No newline at end of file diff --git a/src/main/java/mp/code/intellij/CodeMP.java b/src/main/java/mp/code/intellij/CodeMP.java index 3676a5a..6fb33fe 100644 --- a/src/main/java/mp/code/intellij/CodeMP.java +++ b/src/main/java/mp/code/intellij/CodeMP.java @@ -1,22 +1,33 @@ package mp.code.intellij; +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import com.google.common.collect.Maps; +import com.intellij.openapi.editor.markup.RangeHighlighter; +import mp.code.Extensions; +import mp.code.Workspace; import mp.code.exceptions.ConnectionException; import mp.code.intellij.exceptions.ide.NotConnectedException; -import mp.code.intellij.workspace.IJWorkspace; import mp.code.Client; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.nio.file.Path; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class CodeMP { public static Logger LOGGER = LoggerFactory.getLogger(CodeMP.class); - public static final Map ACTIVE_WORKSPACES = new ConcurrentHashMap<>(); private static Client CLIENT = null; + private static String ACTIVE_WORKSPACE_ID = null; - public static void connect(String url, String username, String password) throws ConnectionException { - CLIENT = Client.connect(url, username, password); + // TODO this sucks + public static BiMap BUFFER_MAPPER = Maps.synchronizedBiMap(HashBiMap.create()); + public static final Map HIGHLIGHTER_MAP = new ConcurrentHashMap<>(); + + public static void connect(String username, String password) throws ConnectionException { + CLIENT = Client.connectToServer(username, password, "api.codemp.dev", 50053, false); // TODO don't hardcode + new Thread(() -> Extensions.drive(true)).start(); } public static void disconnect() { @@ -27,4 +38,27 @@ public class CodeMP { if(CLIENT == null) throw new NotConnectedException(reason); return CLIENT; } + + public static boolean isConnected() { + return CLIENT != null; + } + + public static boolean isInWorkspace() { + return ACTIVE_WORKSPACE_ID != null; + } + + public static Workspace getActiveWorkspace() { + return CodeMP.getClient("get workspace").getWorkspace(ACTIVE_WORKSPACE_ID) + .orElseThrow(IllegalStateException::new); + } + + public static void joinWorkspace(String workspaceId) throws ConnectionException { + CodeMP.getClient("join workspace").joinWorkspace(workspaceId); + ACTIVE_WORKSPACE_ID = workspaceId; + } + + public static void leaveWorkspace() { + CodeMP.getClient("leave workspace").leaveWorkspace(ACTIVE_WORKSPACE_ID); + ACTIVE_WORKSPACE_ID = null; + } } diff --git a/src/main/java/mp/code/intellij/actions/ConnectAction.java b/src/main/java/mp/code/intellij/actions/ConnectAction.java index 27ebc47..fee9289 100644 --- a/src/main/java/mp/code/intellij/actions/ConnectAction.java +++ b/src/main/java/mp/code/intellij/actions/ConnectAction.java @@ -1,39 +1,13 @@ package mp.code.intellij.actions; -import com.intellij.credentialStore.Credentials; -import mp.code.exceptions.ConnectionException; -import mp.code.intellij.CodeMP; -import mp.code.intellij.settings.CodeMPSettings; -import mp.code.intellij.util.ActionUtil; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; +import mp.code.intellij.util.InteractionUtil; import org.jetbrains.annotations.NotNull; -import java.util.Objects; - public class ConnectAction extends AnAction { - public static void connect(AnActionEvent e, boolean silent) throws NullPointerException, ConnectionException { - CodeMPSettings.State state = Objects.requireNonNull(CodeMPSettings.getInstance().getState()); - Credentials creds = Objects.requireNonNull(state.getCredentials()); - CodeMP.connect( - Objects.requireNonNull(state.getServerUrl()), - Objects.requireNonNull(creds.getUserName()), - Objects.requireNonNull(creds.getPasswordAsString()) - ); - - if(!silent) ActionUtil.notify(e, - "Success", String.format("Connected to %s!", state.getServerUrl())); - CodeMP.LOGGER.debug("Connected to {}!", state.getServerUrl()); - } - @Override public void actionPerformed(@NotNull AnActionEvent e) { - try { - connect(e, false); - } catch(NullPointerException ex) { - ActionUtil.notifyError(e, "Invalid credentials!", "Please configure your credentials before connecting."); - } catch(Exception exception) { - ActionUtil.notifyError(e, "Failed to connect to server!", exception); - } + InteractionUtil.connect(e.getProject(), null); } } diff --git a/src/main/java/mp/code/intellij/actions/DisconnectAction.java b/src/main/java/mp/code/intellij/actions/DisconnectAction.java index 51bf2db..b8f1342 100644 --- a/src/main/java/mp/code/intellij/actions/DisconnectAction.java +++ b/src/main/java/mp/code/intellij/actions/DisconnectAction.java @@ -1,26 +1,25 @@ package mp.code.intellij.actions; -import mp.code.intellij.CodeMP; -import mp.code.intellij.util.ActionUtil; +import com.intellij.openapi.actionSystem.ActionUpdateThread; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; +import mp.code.intellij.CodeMP; +import mp.code.intellij.util.InteractionUtil; import org.jetbrains.annotations.NotNull; public class DisconnectAction extends AnAction { - public static void disconnect(AnActionEvent e, boolean silent) { - String url = CodeMP.getClient("disconnect").getUrl(); - CodeMP.disconnect(); - if(!silent) ActionUtil.notify(e, - "Success", String.format("Disconnected from %s!", url)); - CodeMP.LOGGER.debug("Connected to {}!", url); + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + InteractionUtil.disconnect(e.getProject()); } @Override - public void actionPerformed(@NotNull AnActionEvent e) { - try { - disconnect(e, false); - } catch(Exception ex) { - ActionUtil.notifyError(e, "Failed to disconnect!", ex); - } + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabled(CodeMP.isConnected()); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.EDT; } } diff --git a/src/main/java/mp/code/intellij/actions/workspace/WorkspaceJoinAction.java b/src/main/java/mp/code/intellij/actions/workspace/WorkspaceJoinAction.java index 0a7ad49..1344ba0 100644 --- a/src/main/java/mp/code/intellij/actions/workspace/WorkspaceJoinAction.java +++ b/src/main/java/mp/code/intellij/actions/workspace/WorkspaceJoinAction.java @@ -1,55 +1,43 @@ package mp.code.intellij.actions.workspace; -import com.intellij.openapi.module.Module; -import com.intellij.openapi.module.ModuleManager; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.roots.ModuleRootModificationUtil; -import com.intellij.openapi.vfs.VirtualFileManager; -import mp.code.exceptions.ConnectionException; +import com.intellij.openapi.actionSystem.ActionUpdateThread; import mp.code.intellij.CodeMP; -import mp.code.intellij.util.ActionUtil; -import mp.code.intellij.vfs.CodeMPPath; -import mp.code.intellij.vfs.CodeMPFileSystem; -import mp.code.intellij.vfs.CodeMPFolder; +import mp.code.intellij.util.InteractionUtil; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.ui.Messages; -import org.apache.logging.log4j.util.Strings; import org.jetbrains.annotations.NotNull; -import java.io.IOException; - public class WorkspaceJoinAction extends AnAction { - public static void join(AnActionEvent e, String workspaceId, boolean silent) throws ConnectionException, IOException { - CodeMP.getClient("join workspace").joinWorkspace(workspaceId); - CodeMPFileSystem fs = (CodeMPFileSystem) VirtualFileManager.getInstance().getFileSystem(CodeMPFileSystem.PROTOCOL); - CodeMPFolder root = new CodeMPFolder(fs, new CodeMPPath(workspaceId, Strings.EMPTY)); + @Override + public void actionPerformed(@NotNull AnActionEvent e) { + String[] availableWorkspaces = InteractionUtil.listWorkspaces(e.getProject()); + if(availableWorkspaces.length == 0) { + Messages.showErrorDialog( + "There are no available workspaces. Ensure you have rights to access at least one!", + "CodeMP Join Workspace" + ); + } - Project proj = e.getProject(); + int choice = Messages.showDialog( // TODO NOT THE ONE + e.getProject(), + "Please choose a workspace to join:", + "CodeMP Join Workspace", + availableWorkspaces, + 0, + Messages.getQuestionIcon() + ); - assert proj != null; - Module someModule = ModuleManager.getInstance(proj).getModules()[0]; - - ModuleRootModificationUtil.addContentRoot(someModule, root); - - if(!silent) ActionUtil.notify(e, - "Success", String.format("Joined workspace %s!", workspaceId)); - CodeMP.LOGGER.debug("Joined workspace {}!", workspaceId); + InteractionUtil.joinWorkspace(e.getProject(), availableWorkspaces[choice], null); } @Override - public void actionPerformed(@NotNull AnActionEvent e) { - String workspaceId = Messages.showInputDialog( - "Workspace to connect to:", - "CodeMP Join", - Messages.getQuestionIcon()); + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabled(CodeMP.isConnected()); + } - try { - join(e, workspaceId, false); - } catch(Exception ex) { - ActionUtil.notifyError(e, String.format( - "Failed to join workspace %s!", - workspaceId), ex); - } + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.EDT; } } diff --git a/src/main/java/mp/code/intellij/actions/workspace/WorkspaceLeaveAction.java b/src/main/java/mp/code/intellij/actions/workspace/WorkspaceLeaveAction.java index c1d7ea8..da2aa80 100644 --- a/src/main/java/mp/code/intellij/actions/workspace/WorkspaceLeaveAction.java +++ b/src/main/java/mp/code/intellij/actions/workspace/WorkspaceLeaveAction.java @@ -1,22 +1,14 @@ package mp.code.intellij.actions.workspace; -import mp.code.intellij.CodeMP; -import mp.code.intellij.util.ActionUtil; +import com.intellij.openapi.actionSystem.ActionUpdateThread; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.ui.Messages; -import com.intellij.openapi.util.Disposer; +import mp.code.intellij.CodeMP; +import mp.code.intellij.util.InteractionUtil; import org.jetbrains.annotations.NotNull; public class WorkspaceLeaveAction extends AnAction { - public static void leave(AnActionEvent e, String workspaceId, boolean silent) { - CodeMP.getClient("leave workspace").leaveWorkspace(workspaceId); - Disposer.dispose(CodeMP.ACTIVE_WORKSPACES.remove(workspaceId)); - - if(!silent) ActionUtil.notify(e, "Success", String.format("Left workspace %s!", workspaceId)); - CodeMP.LOGGER.debug("Left workspace!"); - } - @Override public void actionPerformed(@NotNull AnActionEvent e) { String workspaceId = Messages.showInputDialog( @@ -24,10 +16,16 @@ public class WorkspaceLeaveAction extends AnAction { "CodeMP Workspace Leave", Messages.getQuestionIcon()); - try { - leave(e, workspaceId, false); - } catch(Exception ex) { - ActionUtil.notifyError(e, "Failed to leave workspace!", ex); - } + InteractionUtil.leaveWorkspace(e.getProject(), workspaceId); + } + + @Override + public void update(@NotNull AnActionEvent e) { + e.getPresentation().setEnabled(CodeMP.isConnected()); + } + + @Override + public @NotNull ActionUpdateThread getActionUpdateThread() { + return ActionUpdateThread.EDT; } } diff --git a/src/main/java/mp/code/intellij/listeners/BufferEventListener.java b/src/main/java/mp/code/intellij/listeners/BufferEventListener.java index 528a349..50bbfae 100644 --- a/src/main/java/mp/code/intellij/listeners/BufferEventListener.java +++ b/src/main/java/mp/code/intellij/listeners/BufferEventListener.java @@ -1,24 +1,22 @@ package mp.code.intellij.listeners; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.EditorFactory; +import com.intellij.openapi.vfs.VirtualFile; import lombok.SneakyThrows; +import mp.code.exceptions.ControllerException; import mp.code.intellij.CodeMP; import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.editor.event.DocumentEvent; import com.intellij.openapi.editor.event.DocumentListener; -import mp.code.BufferController; import mp.code.data.TextChange; import org.jetbrains.annotations.NotNull; +import java.util.Objects; import java.util.OptionalLong; public class BufferEventListener implements DocumentListener { - private final BufferController controller; - - public BufferEventListener(BufferController controller) { - this.controller = controller; - } - @Override @SneakyThrows public void documentChanged(@NotNull DocumentEvent event) { @@ -30,14 +28,28 @@ public class BufferEventListener implements DocumentListener { if(groupString.startsWith("codemp-buffer-receive") || groupString.startsWith("codemp-buffer-sync")) return; - //TODO move actions break - int changeOffset = event.getOffset(); - CharSequence newFragment = event.getNewFragment(); - this.controller.send(new TextChange( - changeOffset, - changeOffset + event.getOldFragment().length(), - newFragment.toString(), - OptionalLong.empty() - )); + VirtualFile file = EditorFactory.getInstance().editors(event.getDocument()) + .map(Editor::getVirtualFile) + .filter(Objects::nonNull) + .filter(vf -> vf.getFileSystem().getNioPath(vf) != null) + .findFirst() + .orElse(null); + + if(file == null) return; + + CodeMP.getActiveWorkspace().getBuffer(CodeMP.BUFFER_MAPPER.get(file.toNioPath())).ifPresent(controller -> { + int changeOffset = event.getOffset(); + CharSequence newFragment = event.getNewFragment(); + try { + controller.send(new TextChange( + changeOffset, + changeOffset + event.getOldFragment().length(), + newFragment.toString(), + OptionalLong.empty() + )); + } catch(ControllerException e) { + throw new RuntimeException(e); + } + }); } } diff --git a/src/main/java/mp/code/intellij/listeners/CursorEventListener.java b/src/main/java/mp/code/intellij/listeners/CursorEventListener.java index 71c378e..63b7572 100644 --- a/src/main/java/mp/code/intellij/listeners/CursorEventListener.java +++ b/src/main/java/mp/code/intellij/listeners/CursorEventListener.java @@ -1,6 +1,11 @@ package mp.code.intellij.listeners; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.application.CoroutinesKt; +import com.intellij.openapi.vfs.VirtualFile; +import com.intellij.util.SlowOperations; import lombok.SneakyThrows; +import mp.code.exceptions.ControllerException; import mp.code.intellij.CodeMP; import mp.code.intellij.util.FileUtil; import com.intellij.openapi.editor.Caret; @@ -8,18 +13,11 @@ import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.VisualPosition; import com.intellij.openapi.editor.event.CaretEvent; import com.intellij.openapi.editor.event.CaretListener; -import mp.code.CursorController; import mp.code.data.Cursor; import org.jetbrains.annotations.NotNull; public class CursorEventListener implements CaretListener { - private final CursorController controller; - - public CursorEventListener(CursorController controller) { - this.controller = controller; - } - @Override @SneakyThrows public void caretPositionChanged(@NotNull CaretEvent event) { @@ -27,20 +25,33 @@ public class CursorEventListener implements CaretListener { if(caret == null) return; + VirtualFile file = event.getEditor().getVirtualFile(); + if(file == null) return; + + if(CodeMP.getActiveWorkspace().getBuffer(CodeMP.BUFFER_MAPPER.get(file.toNioPath())).isEmpty()) return; + VisualPosition startPos = caret.getSelectionStartPosition(); VisualPosition endPos = caret.getSelectionEndPosition(); CodeMP.LOGGER.debug("Caret moved from {}x {}y to {}x {}y", startPos.line, startPos.column, endPos.line, endPos.column ); - Editor editor = event.getEditor(); - this.controller.send(new Cursor( - startPos.line, - startPos.column, - endPos.line, - endPos.column, - FileUtil.getRelativePath(editor.getProject(), editor.getVirtualFile()), - null - )); + new Thread(() -> { // kys + ApplicationManager.getApplication().runReadAction(() -> { + Editor editor = event.getEditor(); + try { + CodeMP.getActiveWorkspace().getCursor().send(new Cursor( + startPos.line, + startPos.column, + endPos.line, + endPos.column, + FileUtil.getRelativePath(editor.getProject(), editor.getVirtualFile()), + null + )); + } catch(ControllerException e) { + throw new RuntimeException(e); + } + }); + }).start(); } } diff --git a/src/main/java/mp/code/intellij/listeners/WorkspaceFileClosedListener.java b/src/main/java/mp/code/intellij/listeners/WorkspaceFileClosedListener.java deleted file mode 100644 index 4b82204..0000000 --- a/src/main/java/mp/code/intellij/listeners/WorkspaceFileClosedListener.java +++ /dev/null @@ -1,33 +0,0 @@ -package mp.code.intellij.listeners; - -import mp.code.intellij.task.BufferEventAwaiterTask; -import mp.code.intellij.util.FileUtil; -import com.intellij.openapi.Disposable; -import com.intellij.openapi.fileEditor.FileEditorManager; -import com.intellij.openapi.fileEditor.FileEditorManagerListener; -import com.intellij.openapi.util.Disposer; -import com.intellij.openapi.vfs.VirtualFile; -import mp.code.Workspace; -import org.jetbrains.annotations.NotNull; - -public class WorkspaceFileClosedListener implements FileEditorManagerListener.Before { - private final Workspace handler; - private final BufferEventAwaiterTask task; - - public WorkspaceFileClosedListener(Workspace handler, BufferEventAwaiterTask task) { - this.handler = handler; - this.task = task; - } - - @Override - public void beforeFileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile file) { - String path = FileUtil.getRelativePath(source.getProject(), file); - if(path == null) return; - - Disposable disp = this.task.activeBuffers.remove(path); - if(disp == null) return; - - this.handler.detachFromBuffer(path); - Disposer.dispose(disp); - } -} diff --git a/src/main/java/mp/code/intellij/listeners/WorkspaceFileOpenedListener.java b/src/main/java/mp/code/intellij/listeners/WorkspaceFileOpenedListener.java deleted file mode 100644 index 9373110..0000000 --- a/src/main/java/mp/code/intellij/listeners/WorkspaceFileOpenedListener.java +++ /dev/null @@ -1,68 +0,0 @@ -package mp.code.intellij.listeners; - -import mp.code.exceptions.ConnectionException; -import mp.code.intellij.task.BufferEventAwaiterTask; -import mp.code.intellij.util.FileUtil; -import com.intellij.openapi.Disposable; -import com.intellij.openapi.fileEditor.FileEditorManager; -import com.intellij.openapi.fileEditor.FileOpenedSyncListener; -import com.intellij.openapi.fileEditor.TextEditor; -import com.intellij.openapi.fileEditor.ex.FileEditorWithProvider; -import com.intellij.openapi.util.Disposer; -import com.intellij.openapi.vfs.VirtualFile; -import mp.code.BufferController; -import mp.code.Workspace; -import org.jetbrains.annotations.NotNull; - -import java.util.List; - -public class WorkspaceFileOpenedListener implements FileOpenedSyncListener { - private final Workspace handler; - private final BufferEventAwaiterTask task; - - public WorkspaceFileOpenedListener(Workspace handler, BufferEventAwaiterTask task) { - this.handler = handler; - this.task = task; - } - - @Override - public void fileOpenedSync(@NotNull FileEditorManager source, - @NotNull VirtualFile file, - @NotNull List editorsWithProviders) { - editorsWithProviders - .stream() - .map(FileEditorWithProvider::component1) - .filter(fe -> fe instanceof TextEditor) - .map(fe -> (TextEditor) fe) - .map(TextEditor::getEditor) - .forEach(editor -> { - String path = FileUtil.getRelativePath(editor.getProject(), file); - if(path == null) return; - - BufferController bufferController = this.getBufferForPath(path); - Disposable disp = Disposer.newDisposable(String.format("codemp-buffer-%s", path)); - editor.getDocument().addDocumentListener(new BufferEventListener(bufferController), disp); - - editor.getDocument().setText(""); //empty it so we can start receiving - this.task.activeBuffers.put(path, disp); - }); - } - - /** - * Attach to a buffer or, if it does not exist, implicitly create it. - * @param path the buffer's name (which is the path relative to project root) - * @return the {@link BufferController} for it - */ - private BufferController getBufferForPath(String path) { - try { - return this.handler.attachToBuffer(path); - } catch (ConnectionException ignored) { - try { - this.handler.createBuffer(path); - return this.handler.attachToBuffer(path); - } catch(ConnectionException e) { - throw new RuntimeException(e); - } - } - } -} diff --git a/src/main/java/mp/code/intellij/task/BufferEventAwaiterTask.java b/src/main/java/mp/code/intellij/task/BufferEventAwaiterTask.java deleted file mode 100644 index 469602f..0000000 --- a/src/main/java/mp/code/intellij/task/BufferEventAwaiterTask.java +++ /dev/null @@ -1,83 +0,0 @@ -package mp.code.intellij.task; - -import lombok.SneakyThrows; -import mp.code.intellij.CodeMP; -import mp.code.intellij.util.FileUtil; -import com.intellij.openapi.Disposable; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.command.CommandProcessor; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.progress.ProgressIndicator; -import com.intellij.openapi.progress.Task; -import com.intellij.openapi.project.Project; -import mp.code.BufferController; -import mp.code.Workspace; -import mp.code.data.TextChange; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - -public class BufferEventAwaiterTask extends Task.Backgroundable implements Disposable { - public final Map activeBuffers; - private final Workspace handler; - public BufferEventAwaiterTask(@NotNull Project project, @NotNull Workspace handler) { - super(project, "Awaiting CodeMP buffer events", false); - this.activeBuffers = new ConcurrentHashMap<>(); - this.handler = handler; - } - - @Override - @SneakyThrows - @SuppressWarnings("InfiniteLoopStatement") - public void run(@NotNull ProgressIndicator indicator) { - while(true) { - Optional bufferOptional; - bufferOptional = this.handler.selectBuffer(100L); - - if(bufferOptional.isEmpty()) - continue; - BufferController buffer = bufferOptional.get(); - - List changeList = new ArrayList<>(); - while(true) { - Optional changeOptional; - try { - changeOptional = buffer.tryRecv(); - } catch(Exception e) { - throw new RuntimeException(e); - } - - if(changeOptional.isEmpty()) - break; - TextChange change = changeOptional.get(); - CodeMP.LOGGER.debug("Received text change {} from offset {} to {}!", - change.content, change.start, change.end); - changeList.add(change); - } - - Editor bufferEditor = FileUtil.getActiveEditorByPath(this.myProject, buffer.getName()); - ApplicationManager.getApplication().invokeLaterOnWriteThread(() -> - ApplicationManager.getApplication().runWriteAction(() -> - CommandProcessor.getInstance().executeCommand( - this.myProject, - () -> changeList.forEach((change) -> - bufferEditor.getDocument().replaceString( - (int) change.start, (int) change.end, change.content) - ), - "CodeMPBufferReceive", - "codemp-buffer-receive", //TODO: mark this with the name - bufferEditor.getDocument() - ))); - } - } - - @Override - public void dispose() { - this.activeBuffers.values().forEach(Disposable::dispose); - this.activeBuffers.clear(); - } -} diff --git a/src/main/java/mp/code/intellij/task/CursorEventAwaiterTask.java b/src/main/java/mp/code/intellij/task/CursorEventAwaiterTask.java deleted file mode 100644 index bb4b211..0000000 --- a/src/main/java/mp/code/intellij/task/CursorEventAwaiterTask.java +++ /dev/null @@ -1,97 +0,0 @@ -package mp.code.intellij.task; - -import lombok.SneakyThrows; -import mp.code.intellij.CodeMP; -import mp.code.intellij.util.ColorUtil; -import mp.code.intellij.util.FileUtil; -import com.intellij.openapi.Disposable; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.RangeMarker; -import com.intellij.openapi.editor.markup.HighlighterLayer; -import com.intellij.openapi.editor.markup.HighlighterTargetArea; -import com.intellij.openapi.editor.markup.RangeHighlighter; -import com.intellij.openapi.editor.markup.TextAttributes; -import com.intellij.openapi.progress.ProgressIndicator; -import com.intellij.openapi.progress.Task; -import com.intellij.openapi.project.Project; -import mp.code.CursorController; -import mp.code.data.Cursor; -import org.jetbrains.annotations.NotNull; - -import java.awt.*; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -//TODO this is janky as it shows a progress bar it doesn't use -//implements disposable so i can use it as lifetime ig -public class CursorEventAwaiterTask extends Task.Backgroundable implements Disposable { - private final CursorController handler; - private final Map highlighterMap = new ConcurrentHashMap<>(); - - public CursorEventAwaiterTask(@NotNull Project project, @NotNull CursorController handler) { - super(project, "Awaiting CodeMP cursor events", false); - this.handler = handler; - } - - @Override - @SneakyThrows - @SuppressWarnings("InfiniteLoopStatement") - public void run(@NotNull ProgressIndicator indicator) { - while(true) { - Cursor event; - event = this.handler.recv(); - Editor editor = FileUtil.getActiveEditorByPath(this.myProject, event.buffer); - if(editor == null) - continue; - - CodeMP.LOGGER.debug( - "Cursor moved by user {}! Start pos: {}x {}y; end pos: {}x {}y in buffer {}!", - event.user, - event.startCol, event.startRow, - event.endCol, event.endRow, - event.buffer - ); - - try { - int startOffset = editor.getDocument() - .getLineStartOffset(event.startRow) + event.startCol; - int endOffset = editor.getDocument() - .getLineStartOffset(event.startRow) + event.startCol; - - ApplicationManager.getApplication().invokeLater(() -> { - int documentLength = editor.getDocument().getTextLength(); - if(startOffset > documentLength || endOffset > documentLength) { - CodeMP.LOGGER.debug( - "Out of bounds cursor: start was {}, end was {}, document length was {}!", - startOffset, endOffset, documentLength); - return; - } - - RangeHighlighter previous = this.highlighterMap.put(event.user, editor - .getMarkupModel() - .addRangeHighlighter( - startOffset, - endOffset, - HighlighterLayer.SELECTION, - new TextAttributes( - null, - ColorUtil.hashColor(event.user), - null, - null, - Font.PLAIN - ), HighlighterTargetArea.EXACT_RANGE - )); - - if(previous != null) - previous.dispose(); - }); - } catch(IndexOutOfBoundsException ignored) {} - } - } - - @Override - public void dispose() { - this.highlighterMap.values().forEach(RangeMarker::dispose); - } -} diff --git a/src/main/java/mp/code/intellij/ui/CodeMPToolWindowFactory.java b/src/main/java/mp/code/intellij/ui/CodeMPToolWindowFactory.java new file mode 100644 index 0000000..55ca030 --- /dev/null +++ b/src/main/java/mp/code/intellij/ui/CodeMPToolWindowFactory.java @@ -0,0 +1,181 @@ +package mp.code.intellij.ui; + +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.command.CommandProcessor; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +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 com.intellij.ui.treeStructure.Tree; +import mp.code.data.TextChange; +import mp.code.exceptions.ControllerException; +import mp.code.intellij.CodeMP; +import mp.code.intellij.util.FileUtil; +import mp.code.intellij.util.InteractionUtil; +import org.jetbrains.annotations.NotNull; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreePath; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class CodeMPToolWindowFactory implements ToolWindowFactory, DumbAware { + @Override + public void createToolWindowContent( + @NotNull Project project, + @NotNull ToolWindow toolWindow + ) { + ContentFactory contentFactory = ContentFactory.getInstance(); + Content content = contentFactory.createContent( + new CodeMPToolWindow(project), + "", + false + ); + + toolWindow.getContentManager().addContent(content); + } + + @Override + public boolean shouldBeAvailable(@NotNull Project project) { + return true; + } + + public static WindowState getWindowState() { + if(!CodeMP.isConnected()) return WindowState.DISCONNECTED; + if(!CodeMP.isInWorkspace()) return WindowState.CONNECTED; + return WindowState.JOINED; + } + + public enum WindowState { + DISCONNECTED, + CONNECTED, + JOINED + } + + public static class CodeMPToolWindow extends JPanel { + public CodeMPToolWindow(Project project) { + this.draw(project); + } + + private void redraw(Project project) { + this.draw(project); + this.repaint(); + } + + private void draw(Project project) { + this.removeAll(); + switch(getWindowState()) { + case DISCONNECTED -> { + JButton connectButton = new JButton(new AbstractAction("Connect...") { + @Override + public void actionPerformed(ActionEvent e) { + InteractionUtil.connect(project, () -> CodeMPToolWindow.this.redraw(project)); + } + }); + this.add(connectButton); + } + case CONNECTED -> { + this.setLayout(new GridLayout(0, 1)); + JTree tree = drawTree(InteractionUtil.listWorkspaces(project)); + tree.addMouseListener(new SimpleMouseListener() { + @Override + public void mouseClicked(MouseEvent e) { + if(e.getClickCount() < 2) return; + TreePath path = tree.getPathForLocation(e.getX(), e.getY()); + if(path == null) return; + String workspaceName = path.getLastPathComponent().toString(); + InteractionUtil.joinWorkspace( + project, + workspaceName, + () -> CodeMPToolWindow.this.redraw(project) + ); + } + }); + this.add(tree); + } + case JOINED -> { + JTree tree = drawTree(CodeMP.getActiveWorkspace().getFileTree(Optional.empty(), false)); + tree.addMouseListener(new SimpleMouseListener() { + @Override + public void mouseClicked(MouseEvent e) { + if(e.getClickCount() < 2) return; + TreePath path = tree.getPathForLocation(e.getX(), e.getY()); + if(path == null) return; + InteractionUtil.bufferAttach( + project, + CodeMP.getActiveWorkspace(), + path.getLastPathComponent().toString() + ).ifPresent(controller -> { + try { + Thread.sleep(1000); // TODO: this sucks + } catch(InterruptedException ignored) {} + ApplicationManager.getApplication().runWriteAction(() -> { + try { + FileUtil.getAndRegisterBufferEquivalent(this, project, controller); + } catch(Exception ex) { + throw new RuntimeException(ex); + } + }); + controller.callback(bufferController -> { + ApplicationManager.getApplication().runReadAction(() -> { + Editor editor = FileUtil.getActiveEditorByPath(project, bufferController.getName()); + ApplicationManager.getApplication().invokeLaterOnWriteThread(() -> { + List changeList = new ArrayList<>(); + while(true) { + Optional changeOptional; + try { + changeOptional = bufferController.tryRecv(); + } catch(ControllerException ex) { + throw new RuntimeException(ex); + } + + if(changeOptional.isEmpty()) + break; + TextChange change = changeOptional.get(); + CodeMP.LOGGER.debug("Received text change {} from offset {} to {}!", + change.content, change.start, change.end); + changeList.add(change); + } + + ApplicationManager.getApplication().runWriteAction(() -> + CommandProcessor.getInstance().executeCommand( + project, + () -> changeList.forEach((change) -> + editor.getDocument().replaceString( + (int) change.start, (int) change.end, change.content) + ), + "CodeMPBufferReceive", + "codemp-buffer-receive", + editor.getDocument() + ) + ); + }); + }); + + }); + }); + } + }); + this.add(tree); + } + } + } + + private JTree drawTree(String[] contents) { + DefaultMutableTreeNode root = new DefaultMutableTreeNode(); + for(String content : contents) { + root.add(new DefaultMutableTreeNode(content)); + } + + return new Tree(root); + } + } +} diff --git a/src/main/java/mp/code/intellij/ui/SimpleMouseListener.java b/src/main/java/mp/code/intellij/ui/SimpleMouseListener.java new file mode 100644 index 0000000..a34fa14 --- /dev/null +++ b/src/main/java/mp/code/intellij/ui/SimpleMouseListener.java @@ -0,0 +1,30 @@ +package mp.code.intellij.ui; + +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; + +/** + * Allows usage of {@link MouseListener} without implementing + * all methods. + */ +class SimpleMouseListener implements MouseListener { + @Override + public void mouseClicked(MouseEvent e) { + } + + @Override + public void mousePressed(MouseEvent e) { + } + + @Override + public void mouseReleased(MouseEvent e) { + } + + @Override + public void mouseEntered(MouseEvent e) { + } + + @Override + public void mouseExited(MouseEvent e) { + } +} diff --git a/src/main/java/mp/code/intellij/util/ActionUtil.java b/src/main/java/mp/code/intellij/util/ActionUtil.java deleted file mode 100644 index a579d3c..0000000 --- a/src/main/java/mp/code/intellij/util/ActionUtil.java +++ /dev/null @@ -1,49 +0,0 @@ -package mp.code.intellij.util; - -import mp.code.intellij.CodeMP; -import mp.code.intellij.exceptions.ide.BadActionEventStateException; -import com.intellij.notification.Notification; -import com.intellij.notification.NotificationType; -import com.intellij.notification.Notifications; -import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.fileEditor.FileEditorManager; -import com.intellij.openapi.project.Project; - -public class ActionUtil { - public static Project getCurrentProject(AnActionEvent event) { - Project project = event.getProject(); - if(project == null) - throw new BadActionEventStateException("Project was null!"); - return project; - } - - public static Editor getCurrentEditor(AnActionEvent event) { - Editor editor = FileEditorManager.getInstance(getCurrentProject(event)) - .getSelectedTextEditor(); - if(editor == null) - throw new BadActionEventStateException("Editor was null!"); - return editor; - } - - public static void notify(AnActionEvent event, String title, String msg) { - Notifications.Bus.notify(new Notification( - "CodeMP", title, msg, NotificationType.INFORMATION - ), event.getProject()); - } - - public static void notifyError(AnActionEvent event, String title, String msg) { - Notifications.Bus.notify(new Notification( - "CodeMP", title, msg, NotificationType.ERROR - ), event.getProject()); - } - - public static void notifyError(AnActionEvent event, String title, Throwable t) { - Notifications.Bus.notify(new Notification( - "CodeMP", title, - String.format("%s: %s", t.getClass().getCanonicalName(), t.getMessage()), - NotificationType.ERROR - ), event.getProject()); - CodeMP.LOGGER.error(title, t); - } -} diff --git a/src/main/java/mp/code/intellij/util/FileUtil.java b/src/main/java/mp/code/intellij/util/FileUtil.java index 9792b83..0b8eab9 100644 --- a/src/main/java/mp/code/intellij/util/FileUtil.java +++ b/src/main/java/mp/code/intellij/util/FileUtil.java @@ -1,7 +1,9 @@ package mp.code.intellij.util; import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.fileEditor.FileEditor; import com.intellij.openapi.fileEditor.FileEditorManager; +import com.intellij.openapi.fileEditor.OpenFileDescriptor; import com.intellij.openapi.fileEditor.TextEditor; import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ProjectRootManager; @@ -9,9 +11,11 @@ import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; import mp.code.BufferController; import mp.code.exceptions.ConnectionException; +import mp.code.exceptions.ControllerException; import mp.code.intellij.CodeMP; import mp.code.intellij.vfs.CodeMPPath; +import java.io.IOException; import java.util.Arrays; import java.util.Optional; @@ -35,6 +39,29 @@ public class FileUtil { .orElse(null); } + public static FileEditor getAndRegisterBufferEquivalent(Object requestor, Project project, BufferController buffer) throws ControllerException, IOException { + VirtualFile contentRoot = ProjectRootManager.getInstance(project).getContentRoots()[0]; + String bufferName = buffer.getName(); + + VirtualFile found = contentRoot.findFileByRelativePath(bufferName); + if(found == null) { + VirtualFile lastParent = contentRoot; + String[] path = bufferName.split("/"); + for(int i = 0; i < path.length - 1; i++) + lastParent = lastParent.createChildDirectory(requestor, path[i]); + found = lastParent.createChildData(requestor, path[path.length - 1]); + } + + found.setBinaryContent(buffer.getContent().getBytes()); + + CodeMP.BUFFER_MAPPER.put(found.toNioPath(), bufferName); + + return FileEditorManager.getInstance(project).openEditor( + new OpenFileDescriptor(project, found, 0), + true + ).get(0); + } + /** * Will first check if such a buffer exists. * If it does, it will try to get the relevant controller and, diff --git a/src/main/java/mp/code/intellij/util/InteractionUtil.java b/src/main/java/mp/code/intellij/util/InteractionUtil.java new file mode 100644 index 0000000..b30d555 --- /dev/null +++ b/src/main/java/mp/code/intellij/util/InteractionUtil.java @@ -0,0 +1,231 @@ +package mp.code.intellij.util; + +import com.intellij.credentialStore.Credentials; +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.editor.Editor; +import com.intellij.openapi.editor.EditorFactory; +import com.intellij.openapi.editor.event.EditorEventMulticaster; +import com.intellij.openapi.editor.markup.HighlighterLayer; +import com.intellij.openapi.editor.markup.HighlighterTargetArea; +import com.intellij.openapi.editor.markup.RangeHighlighter; +import com.intellij.openapi.editor.markup.TextAttributes; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.progress.ProgressManager; +import com.intellij.openapi.progress.Task; +import com.intellij.openapi.project.Project; +import mp.code.BufferController; +import mp.code.Client; +import mp.code.Workspace; +import mp.code.data.Cursor; +import mp.code.exceptions.ConnectionException; +import mp.code.exceptions.ConnectionRemoteException; +import mp.code.exceptions.ControllerException; +import mp.code.intellij.CodeMP; +import mp.code.intellij.listeners.BufferEventListener; +import mp.code.intellij.listeners.CursorEventListener; +import mp.code.intellij.settings.CodeMPSettings; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.awt.*; +import java.util.Objects; +import java.util.Optional; + +/** + * Contains utility classes for interacting with CodeMP in contexts + * where the user should be shown the output, taking care of things + * like notifications and error handling. + */ +public class InteractionUtil { + public static void connect(@Nullable Project project, @Nullable Runnable after) { + ProgressManager.getInstance().run(new Task.Backgroundable(project, "Connecting to CodeMP server...") { + @Override + public void run(@NotNull ProgressIndicator indicator) { + try { + CodeMPSettings.State state = Objects.requireNonNull(CodeMPSettings.getInstance().getState()); + Credentials credentials = Objects.requireNonNull(state.getCredentials()); + CodeMP.connect( + Objects.requireNonNull(credentials.getUserName()), + Objects.requireNonNull(credentials.getPasswordAsString()) + ); + + if(after != null) after.run(); + + notifyInfo( + project, + "Success", + String.format("Connected to %s!", state.getServerUrl()) + ); + } catch(NullPointerException e) { + Notifications.Bus.notify(new Notification( + "CodeMP", + "Invalid credentials!", + "Please configure your credentials before connecting.", + NotificationType.ERROR + ), project); + } catch(ConnectionException e) { + notifyError(project, "Failed to leave workspace!", e); + } + } + }); + } + + public static void disconnect(@Nullable Project project) { + CodeMP.disconnect(); + notifyInfo(project, "Success", "Disconnected from server!"); + } + + public static void joinWorkspace(Project project, @NotNull String workspaceId, @Nullable Runnable after) { + ProgressManager.getInstance().run(new Task.Backgroundable(project, String.format("Joining workspace %s...", workspaceId)) { + @Override + public void run(@NotNull ProgressIndicator indicator) { + if(project == null) { + Notifications.Bus.notify(new Notification( + "CodeMP", + "No project found", + "Please ensure that you have an open project before attempting to join a workspace.", + NotificationType.ERROR + ), null); + return; + } + + try { + CodeMP.joinWorkspace(workspaceId); + } catch(ConnectionException e) { + InteractionUtil.notifyError(project, String.format( + "Failed to join workspace %s!", + workspaceId + ), e); + return; + } + + EditorEventMulticaster eventMulticaster = EditorFactory.getInstance().getEventMulticaster(); + + eventMulticaster.addDocumentListener(new BufferEventListener()); // TODO disposable + eventMulticaster.addCaretListener(new CursorEventListener()); // TODO disposable + + CodeMP.getActiveWorkspace().getCursor().callback(controller -> { + ApplicationManager.getApplication().invokeLater(() -> { + try { + while(true) { + Optional c = controller.tryRecv(); + if(c.isEmpty()) break; + Cursor event = c.get(); + + CodeMP.LOGGER.debug( + "Cursor moved by user {}! Start pos: {}x {}y; end pos: {}x {}y in buffer {}!", + event.user, + event.startCol, event.startRow, + event.endCol, event.endRow, + event.buffer + ); + + try { + ApplicationManager.getApplication().invokeLater(() -> { + Editor editor = FileUtil.getActiveEditorByPath(this.myProject, event.buffer); + if(editor == null) return; + + int startOffset = editor.getDocument() + .getLineStartOffset(event.startRow) + event.startCol; + int endOffset = editor.getDocument() + .getLineStartOffset(event.startRow) + event.startCol; + + int documentLength = editor.getDocument().getTextLength(); + if(startOffset > documentLength || endOffset > documentLength) { + CodeMP.LOGGER.debug( + "Out of bounds cursor: start was {}, end was {}, document length was {}!", + startOffset, endOffset, documentLength); + return; + } + + RangeHighlighter previous = CodeMP.HIGHLIGHTER_MAP.put( + event.user, + editor.getMarkupModel().addRangeHighlighter( + startOffset, + endOffset, + HighlighterLayer.SELECTION, + new TextAttributes( + null, + ColorUtil.hashColor(event.user), + null, + null, + Font.PLAIN + ), HighlighterTargetArea.EXACT_RANGE + ) + ); + + if(previous != null) + previous.dispose(); + }); + } catch(IndexOutOfBoundsException ignored) {} + } + } catch(ControllerException ex) { + notifyError(project, "Error receiving change", ex); + } + }); + }); + + if(after != null) after.run(); + + notifyInfo( + project, + "Success", + String.format("Joined workspace %s!", workspaceId) + ); + } + }); + } + + public static void leaveWorkspace(Project project, String workspaceId) { + CodeMP.leaveWorkspace(); + notifyInfo( + project, + "Success", + String.format("Left workspace %s!", workspaceId) + ); + } + + public static String[] listWorkspaces(Project project) { + try { + Client client = CodeMP.getClient("drawActiveWorkspaces"); + return client.listWorkspaces(true, true); + } catch(ConnectionRemoteException exception) { + notifyError(project, "Failed to list workspaces!", exception); + return new String[0]; + } + } + + public static Optional bufferAttach(Project project, Workspace workspace, String path) { + try { + BufferController controller = workspace.attachToBuffer(path); + notifyInfo(project, "Success!", String.format( + "Successfully attached to buffer %s on workspace %s!", + path, + workspace.getWorkspaceId()) + ); + + return Optional.of(controller); + } catch(ConnectionException e) { + notifyError(project, "Failed to attach to buffer!", e); + return Optional.empty(); + } + } + + private static void notifyInfo(Project project, String title, String msg) { + Notifications.Bus.notify(new Notification( + "CodeMP", title, msg, NotificationType.INFORMATION + ), project); + } + + private static void notifyError(Project project, String title, Throwable t) { + Notifications.Bus.notify(new Notification( + "CodeMP", title, + String.format("%s: %s", t.getClass().getCanonicalName(), t.getMessage()), + NotificationType.ERROR + ), project); + CodeMP.LOGGER.error(title, t); + } +} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 1f5b91a..9d7a4b9 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -33,5 +33,10 @@ id="mp.code.intellij.settings.CodeMPSettingsConfigurable" displayName="CodeMP"/> +