mirror of
https://github.com/hexedtech/codemp-intellij.git
synced 2024-11-23 15:44:48 +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
|
.cargo
|
||||||
target
|
target
|
||||||
|
|
||||||
#IntelliJ test client run config
|
# IntelliJ test client run config
|
||||||
.run/
|
.run/
|
||||||
|
|
||||||
|
# Stuff from IntellIJ plugin
|
||||||
|
.intellijPlatform
|
||||||
|
|
||||||
# Do not include generated code
|
# Do not include generated code
|
||||||
src/main/java/com/codemp/intellij/jni
|
src/main/java/com/codemp/intellij/jni
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
[![codemp](https://code.mp/static/banner.png)](https://code.mp)
|
[![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,
|
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.
|
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;
|
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.exceptions.ConnectionException;
|
||||||
import mp.code.intellij.exceptions.ide.NotConnectedException;
|
import mp.code.intellij.exceptions.ide.NotConnectedException;
|
||||||
import mp.code.intellij.workspace.IJWorkspace;
|
|
||||||
import mp.code.Client;
|
import mp.code.Client;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
public class CodeMP {
|
public class CodeMP {
|
||||||
public static Logger LOGGER = LoggerFactory.getLogger(CodeMP.class);
|
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 Client CLIENT = null;
|
||||||
|
private static String ACTIVE_WORKSPACE_ID = null;
|
||||||
|
|
||||||
public static void connect(String url, String username, String password) throws ConnectionException {
|
// TODO this sucks
|
||||||
CLIENT = Client.connect(url, username, password);
|
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() {
|
public static void disconnect() {
|
||||||
|
@ -27,4 +38,27 @@ public class CodeMP {
|
||||||
if(CLIENT == null) throw new NotConnectedException(reason);
|
if(CLIENT == null) throw new NotConnectedException(reason);
|
||||||
return CLIENT;
|
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;
|
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.AnAction;
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
import com.intellij.openapi.actionSystem.AnActionEvent;
|
||||||
|
import mp.code.intellij.util.InteractionUtil;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
public class ConnectAction extends AnAction {
|
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
|
@Override
|
||||||
public void actionPerformed(@NotNull AnActionEvent e) {
|
public void actionPerformed(@NotNull AnActionEvent e) {
|
||||||
try {
|
InteractionUtil.connect(e.getProject(), null);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,26 +1,25 @@
|
||||||
package mp.code.intellij.actions;
|
package mp.code.intellij.actions;
|
||||||
|
|
||||||
import mp.code.intellij.CodeMP;
|
import com.intellij.openapi.actionSystem.ActionUpdateThread;
|
||||||
import mp.code.intellij.util.ActionUtil;
|
|
||||||
import com.intellij.openapi.actionSystem.AnAction;
|
import com.intellij.openapi.actionSystem.AnAction;
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
import com.intellij.openapi.actionSystem.AnActionEvent;
|
||||||
|
import mp.code.intellij.CodeMP;
|
||||||
|
import mp.code.intellij.util.InteractionUtil;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class DisconnectAction extends AnAction {
|
public class DisconnectAction extends AnAction {
|
||||||
public static void disconnect(AnActionEvent e, boolean silent) {
|
@Override
|
||||||
String url = CodeMP.getClient("disconnect").getUrl();
|
public void actionPerformed(@NotNull AnActionEvent e) {
|
||||||
CodeMP.disconnect();
|
InteractionUtil.disconnect(e.getProject());
|
||||||
if(!silent) ActionUtil.notify(e,
|
|
||||||
"Success", String.format("Disconnected from %s!", url));
|
|
||||||
CodeMP.LOGGER.debug("Connected to {}!", url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void actionPerformed(@NotNull AnActionEvent e) {
|
public void update(@NotNull AnActionEvent e) {
|
||||||
try {
|
e.getPresentation().setEnabled(CodeMP.isConnected());
|
||||||
disconnect(e, false);
|
}
|
||||||
} catch(Exception ex) {
|
|
||||||
ActionUtil.notifyError(e, "Failed to disconnect!", ex);
|
@Override
|
||||||
}
|
public @NotNull ActionUpdateThread getActionUpdateThread() {
|
||||||
|
return ActionUpdateThread.EDT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,55 +1,43 @@
|
||||||
package mp.code.intellij.actions.workspace;
|
package mp.code.intellij.actions.workspace;
|
||||||
|
|
||||||
import com.intellij.openapi.module.Module;
|
import com.intellij.openapi.actionSystem.ActionUpdateThread;
|
||||||
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 mp.code.intellij.CodeMP;
|
import mp.code.intellij.CodeMP;
|
||||||
import mp.code.intellij.util.ActionUtil;
|
import mp.code.intellij.util.InteractionUtil;
|
||||||
import mp.code.intellij.vfs.CodeMPPath;
|
|
||||||
import mp.code.intellij.vfs.CodeMPFileSystem;
|
|
||||||
import mp.code.intellij.vfs.CodeMPFolder;
|
|
||||||
import com.intellij.openapi.actionSystem.AnAction;
|
import com.intellij.openapi.actionSystem.AnAction;
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
import com.intellij.openapi.actionSystem.AnActionEvent;
|
||||||
import com.intellij.openapi.ui.Messages;
|
import com.intellij.openapi.ui.Messages;
|
||||||
import org.apache.logging.log4j.util.Strings;
|
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
public class WorkspaceJoinAction extends AnAction {
|
public class WorkspaceJoinAction extends AnAction {
|
||||||
public static void join(AnActionEvent e, String workspaceId, boolean silent) throws ConnectionException, IOException {
|
@Override
|
||||||
CodeMP.getClient("join workspace").joinWorkspace(workspaceId);
|
public void actionPerformed(@NotNull AnActionEvent e) {
|
||||||
CodeMPFileSystem fs = (CodeMPFileSystem) VirtualFileManager.getInstance().getFileSystem(CodeMPFileSystem.PROTOCOL);
|
String[] availableWorkspaces = InteractionUtil.listWorkspaces(e.getProject());
|
||||||
CodeMPFolder root = new CodeMPFolder(fs, new CodeMPPath(workspaceId, Strings.EMPTY));
|
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;
|
InteractionUtil.joinWorkspace(e.getProject(), availableWorkspaces[choice], 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void actionPerformed(@NotNull AnActionEvent e) {
|
public void update(@NotNull AnActionEvent e) {
|
||||||
String workspaceId = Messages.showInputDialog(
|
e.getPresentation().setEnabled(CodeMP.isConnected());
|
||||||
"Workspace to connect to:",
|
}
|
||||||
"CodeMP Join",
|
|
||||||
Messages.getQuestionIcon());
|
|
||||||
|
|
||||||
try {
|
@Override
|
||||||
join(e, workspaceId, false);
|
public @NotNull ActionUpdateThread getActionUpdateThread() {
|
||||||
} catch(Exception ex) {
|
return ActionUpdateThread.EDT;
|
||||||
ActionUtil.notifyError(e, String.format(
|
|
||||||
"Failed to join workspace %s!",
|
|
||||||
workspaceId), ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +1,14 @@
|
||||||
package mp.code.intellij.actions.workspace;
|
package mp.code.intellij.actions.workspace;
|
||||||
|
|
||||||
import mp.code.intellij.CodeMP;
|
import com.intellij.openapi.actionSystem.ActionUpdateThread;
|
||||||
import mp.code.intellij.util.ActionUtil;
|
|
||||||
import com.intellij.openapi.actionSystem.AnAction;
|
import com.intellij.openapi.actionSystem.AnAction;
|
||||||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
import com.intellij.openapi.actionSystem.AnActionEvent;
|
||||||
import com.intellij.openapi.ui.Messages;
|
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;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class WorkspaceLeaveAction extends AnAction {
|
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
|
@Override
|
||||||
public void actionPerformed(@NotNull AnActionEvent e) {
|
public void actionPerformed(@NotNull AnActionEvent e) {
|
||||||
String workspaceId = Messages.showInputDialog(
|
String workspaceId = Messages.showInputDialog(
|
||||||
|
@ -24,10 +16,16 @@ public class WorkspaceLeaveAction extends AnAction {
|
||||||
"CodeMP Workspace Leave",
|
"CodeMP Workspace Leave",
|
||||||
Messages.getQuestionIcon());
|
Messages.getQuestionIcon());
|
||||||
|
|
||||||
try {
|
InteractionUtil.leaveWorkspace(e.getProject(), workspaceId);
|
||||||
leave(e, workspaceId, false);
|
}
|
||||||
} catch(Exception ex) {
|
|
||||||
ActionUtil.notifyError(e, "Failed to leave workspace!", ex);
|
@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;
|
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 lombok.SneakyThrows;
|
||||||
|
import mp.code.exceptions.ControllerException;
|
||||||
import mp.code.intellij.CodeMP;
|
import mp.code.intellij.CodeMP;
|
||||||
import com.intellij.openapi.command.CommandProcessor;
|
import com.intellij.openapi.command.CommandProcessor;
|
||||||
import com.intellij.openapi.editor.event.DocumentEvent;
|
import com.intellij.openapi.editor.event.DocumentEvent;
|
||||||
import com.intellij.openapi.editor.event.DocumentListener;
|
import com.intellij.openapi.editor.event.DocumentListener;
|
||||||
import mp.code.BufferController;
|
|
||||||
import mp.code.data.TextChange;
|
import mp.code.data.TextChange;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
import java.util.OptionalLong;
|
import java.util.OptionalLong;
|
||||||
|
|
||||||
public class BufferEventListener implements DocumentListener {
|
public class BufferEventListener implements DocumentListener {
|
||||||
|
|
||||||
private final BufferController controller;
|
|
||||||
|
|
||||||
public BufferEventListener(BufferController controller) {
|
|
||||||
this.controller = controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public void documentChanged(@NotNull DocumentEvent event) {
|
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"))
|
if(groupString.startsWith("codemp-buffer-receive") || groupString.startsWith("codemp-buffer-sync"))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
//TODO move actions break
|
VirtualFile file = EditorFactory.getInstance().editors(event.getDocument())
|
||||||
int changeOffset = event.getOffset();
|
.map(Editor::getVirtualFile)
|
||||||
CharSequence newFragment = event.getNewFragment();
|
.filter(Objects::nonNull)
|
||||||
this.controller.send(new TextChange(
|
.filter(vf -> vf.getFileSystem().getNioPath(vf) != null)
|
||||||
changeOffset,
|
.findFirst()
|
||||||
changeOffset + event.getOldFragment().length(),
|
.orElse(null);
|
||||||
newFragment.toString(),
|
|
||||||
OptionalLong.empty()
|
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;
|
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 lombok.SneakyThrows;
|
||||||
|
import mp.code.exceptions.ControllerException;
|
||||||
import mp.code.intellij.CodeMP;
|
import mp.code.intellij.CodeMP;
|
||||||
import mp.code.intellij.util.FileUtil;
|
import mp.code.intellij.util.FileUtil;
|
||||||
import com.intellij.openapi.editor.Caret;
|
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.VisualPosition;
|
||||||
import com.intellij.openapi.editor.event.CaretEvent;
|
import com.intellij.openapi.editor.event.CaretEvent;
|
||||||
import com.intellij.openapi.editor.event.CaretListener;
|
import com.intellij.openapi.editor.event.CaretListener;
|
||||||
import mp.code.CursorController;
|
|
||||||
import mp.code.data.Cursor;
|
import mp.code.data.Cursor;
|
||||||
import org.jetbrains.annotations.NotNull;
|
import org.jetbrains.annotations.NotNull;
|
||||||
|
|
||||||
public class CursorEventListener implements CaretListener {
|
public class CursorEventListener implements CaretListener {
|
||||||
|
|
||||||
private final CursorController controller;
|
|
||||||
|
|
||||||
public CursorEventListener(CursorController controller) {
|
|
||||||
this.controller = controller;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public void caretPositionChanged(@NotNull CaretEvent event) {
|
public void caretPositionChanged(@NotNull CaretEvent event) {
|
||||||
|
@ -27,20 +25,33 @@ public class CursorEventListener implements CaretListener {
|
||||||
if(caret == null)
|
if(caret == null)
|
||||||
return;
|
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 startPos = caret.getSelectionStartPosition();
|
||||||
VisualPosition endPos = caret.getSelectionEndPosition();
|
VisualPosition endPos = caret.getSelectionEndPosition();
|
||||||
CodeMP.LOGGER.debug("Caret moved from {}x {}y to {}x {}y",
|
CodeMP.LOGGER.debug("Caret moved from {}x {}y to {}x {}y",
|
||||||
startPos.line, startPos.column, endPos.line, endPos.column
|
startPos.line, startPos.column, endPos.line, endPos.column
|
||||||
);
|
);
|
||||||
|
|
||||||
Editor editor = event.getEditor();
|
new Thread(() -> { // kys
|
||||||
this.controller.send(new Cursor(
|
ApplicationManager.getApplication().runReadAction(() -> {
|
||||||
startPos.line,
|
Editor editor = event.getEditor();
|
||||||
startPos.column,
|
try {
|
||||||
endPos.line,
|
CodeMP.getActiveWorkspace().getCursor().send(new Cursor(
|
||||||
endPos.column,
|
startPos.line,
|
||||||
FileUtil.getRelativePath(editor.getProject(), editor.getVirtualFile()),
|
startPos.column,
|
||||||
null
|
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;
|
package mp.code.intellij.util;
|
||||||
|
|
||||||
import com.intellij.openapi.editor.Editor;
|
import com.intellij.openapi.editor.Editor;
|
||||||
|
import com.intellij.openapi.fileEditor.FileEditor;
|
||||||
import com.intellij.openapi.fileEditor.FileEditorManager;
|
import com.intellij.openapi.fileEditor.FileEditorManager;
|
||||||
|
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
|
||||||
import com.intellij.openapi.fileEditor.TextEditor;
|
import com.intellij.openapi.fileEditor.TextEditor;
|
||||||
import com.intellij.openapi.project.Project;
|
import com.intellij.openapi.project.Project;
|
||||||
import com.intellij.openapi.roots.ProjectRootManager;
|
import com.intellij.openapi.roots.ProjectRootManager;
|
||||||
|
@ -9,9 +11,11 @@ import com.intellij.openapi.vfs.VfsUtilCore;
|
||||||
import com.intellij.openapi.vfs.VirtualFile;
|
import com.intellij.openapi.vfs.VirtualFile;
|
||||||
import mp.code.BufferController;
|
import mp.code.BufferController;
|
||||||
import mp.code.exceptions.ConnectionException;
|
import mp.code.exceptions.ConnectionException;
|
||||||
|
import mp.code.exceptions.ControllerException;
|
||||||
import mp.code.intellij.CodeMP;
|
import mp.code.intellij.CodeMP;
|
||||||
import mp.code.intellij.vfs.CodeMPPath;
|
import mp.code.intellij.vfs.CodeMPPath;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
@ -35,6 +39,29 @@ public class FileUtil {
|
||||||
.orElse(null);
|
.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.
|
* Will first check if such a buffer exists.
|
||||||
* If it does, it will try to get the relevant controller and,
|
* 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"
|
id="mp.code.intellij.settings.CodeMPSettingsConfigurable"
|
||||||
displayName="CodeMP"/>
|
displayName="CodeMP"/>
|
||||||
<workspaceModel.fileIndexContributor implementation="mp.code.intellij.vfs.CodeMPFileIndexContributor" />
|
<workspaceModel.fileIndexContributor implementation="mp.code.intellij.vfs.CodeMPFileIndexContributor" />
|
||||||
|
<toolWindow
|
||||||
|
id="CodeMP"
|
||||||
|
factoryClass="mp.code.intellij.ui.CodeMPToolWindowFactory"
|
||||||
|
anchor="right"
|
||||||
|
doNotActivateOnStart="false" />
|
||||||
</extensions>
|
</extensions>
|
||||||
</idea-plugin>
|
</idea-plugin>
|
||||||
|
|
Loading…
Reference in a new issue