mirror of
https://github.com/hexedtech/codemp-intellij.git
synced 2024-12-23 13:04:52 +01:00
feat(java): brought the plugin to a broadly usable state
This commit is contained in:
parent
44d5c77383
commit
bbc2eb447a
20 changed files with 632 additions and 465 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
|
@ -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<String, IJWorkspace> 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<Path, String> BUFFER_MAPPER = Maps.synchronizedBiMap(HashBiMap.create());
|
||||
public static final Map<String, RangeHighlighter> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<FileEditorWithProvider> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String, Disposable> 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<BufferController> bufferOptional;
|
||||
bufferOptional = this.handler.selectBuffer(100L);
|
||||
|
||||
if(bufferOptional.isEmpty())
|
||||
continue;
|
||||
BufferController buffer = bufferOptional.get();
|
||||
|
||||
List<TextChange> changeList = new ArrayList<>();
|
||||
while(true) {
|
||||
Optional<TextChange> 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();
|
||||
}
|
||||
}
|
|
@ -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<String, RangeHighlighter> 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);
|
||||
}
|
||||
}
|
181
src/main/java/mp/code/intellij/ui/CodeMPToolWindowFactory.java
Normal file
181
src/main/java/mp/code/intellij/ui/CodeMPToolWindowFactory.java
Normal file
|
@ -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<TextChange> changeList = new ArrayList<>();
|
||||
while(true) {
|
||||
Optional<TextChange> 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);
|
||||
}
|
||||
}
|
||||
}
|
30
src/main/java/mp/code/intellij/ui/SimpleMouseListener.java
Normal file
30
src/main/java/mp/code/intellij/ui/SimpleMouseListener.java
Normal file
|
@ -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) {
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
231
src/main/java/mp/code/intellij/util/InteractionUtil.java
Normal file
231
src/main/java/mp/code/intellij/util/InteractionUtil.java
Normal file
|
@ -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<Cursor> 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<BufferController> 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);
|
||||
}
|
||||
}
|
|
@ -33,5 +33,10 @@
|
|||
id="mp.code.intellij.settings.CodeMPSettingsConfigurable"
|
||||
displayName="CodeMP"/>
|
||||
<workspaceModel.fileIndexContributor implementation="mp.code.intellij.vfs.CodeMPFileIndexContributor" />
|
||||
<toolWindow
|
||||
id="CodeMP"
|
||||
factoryClass="mp.code.intellij.ui.CodeMPToolWindowFactory"
|
||||
anchor="right"
|
||||
doNotActivateOnStart="false" />
|
||||
</extensions>
|
||||
</idea-plugin>
|
||||
|
|
Loading…
Reference in a new issue