feat: workspace support, initial impl

This commit is contained in:
zaaarf 2024-01-31 22:56:45 +01:00
parent 085488ba36
commit 7c5c851f6f
No known key found for this signature in database
GPG key ID: 102E445F4C3F829B
24 changed files with 324 additions and 146 deletions

View file

@ -4,7 +4,8 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", branch = "workspace", features = ["global", "sync"] } codemp = { path = "../../lib", features = ["global", "sync", "transport"] }
#codemp = { git = "ssh://git@github.com/codewithotherpeopleandchangenamelater/codemp.git", branch = "workspace", features = ["global", "sync"] }
jni = { version = "0.21.1", features = ["invocation"] } jni = { version = "0.21.1", features = ["invocation"] }
jni-sys = "0.3.0" jni-sys = "0.3.0"
lazy_static = "1.4.0" lazy_static = "1.4.0"

View file

@ -19,11 +19,11 @@ repositories {
dependencies { dependencies {
implementation 'com.github.adamheinrich:native-utils:master-SNAPSHOT' implementation 'com.github.adamheinrich:native-utils:master-SNAPSHOT'
implementation 'org.slf4j:slf4j-api:2.0.9' implementation 'org.slf4j:slf4j-api:2.0.9'
implementation 'ch.qos.logback:logback-classic:1.4.6' implementation 'ch.qos.logback:logback-classic:1.4.12'
} }
intellij { intellij {
version.set('2022.2.5') version.set('2023.3')
type.set('IC') type.set('IC')
} }

View file

@ -1,6 +1,8 @@
package com.codemp.intellij; package com.codemp.intellij;
import com.intellij.openapi.editor.Editor; import com.codemp.intellij.exceptions.ide.NotConnectedException;
import com.codemp.intellij.jni.ClientHandler;
import com.codemp.intellij.workspace.Workspace;
import com.intellij.openapi.util.SystemInfo; import com.intellij.openapi.util.SystemInfo;
import cz.adamh.utils.NativeUtils; import cz.adamh.utils.NativeUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -12,11 +14,25 @@ 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, Workspace> ACTIVE_WORKSPACES = new ConcurrentHashMap<>();
private static ClientHandler CLIENT = null;
public static final Map<String, Editor> ACTIVE_BUFFERS = new ConcurrentHashMap<>(); public static void connect(String url) {
public static final Map<Editor, String> ACTIVE_BUFFERS_REVERSE = new ConcurrentHashMap<>(); CodeMP.loadLibrary(); //will only load it the first time
CLIENT = new ClientHandler(url);
}
public static void disconnect() {
CLIENT = null;
}
public static ClientHandler getClient(String reason) throws NotConnectedException {
if(CLIENT == null) throw new NotConnectedException(reason);
return CLIENT;
}
private static boolean loadedLibrary = false; private static boolean loadedLibrary = false;
public static void loadLibrary() { public static void loadLibrary() {
if(!loadedLibrary) { if(!loadedLibrary) {
try { try {

View file

@ -1,7 +1,6 @@
package com.codemp.intellij.actions; package com.codemp.intellij.actions;
import com.codemp.intellij.CodeMP; import com.codemp.intellij.CodeMP;
import com.codemp.intellij.jni.CodeMPHandler;
import com.codemp.intellij.util.ActionUtil; import com.codemp.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;
@ -10,8 +9,7 @@ import org.jetbrains.annotations.NotNull;
public class ConnectAction extends AnAction { public class ConnectAction extends AnAction {
public static void connect(AnActionEvent e, String url, boolean silent) { public static void connect(AnActionEvent e, String url, boolean silent) {
CodeMP.loadLibrary(); //will only load it the first time CodeMP.connect(url);
CodeMPHandler.connect(url);
if(!silent) ActionUtil.notify(e, if(!silent) ActionUtil.notify(e,
"Success", String.format("Connected to %s!", url)); "Success", String.format("Connected to %s!", url));

View file

@ -0,0 +1,28 @@
package com.codemp.intellij.actions;
import com.codemp.intellij.CodeMP;
import com.codemp.intellij.util.ActionUtil;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
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) {
try {
disconnect(e, false);
} catch(Exception ex) {
ActionUtil.notifyError(e, "Failed to disconnect!", ex);
}
}
}

View file

@ -2,8 +2,6 @@ package com.codemp.intellij.actions.buffer;
import com.codemp.intellij.CodeMP; import com.codemp.intellij.CodeMP;
import com.codemp.intellij.jni.BufferHandler; import com.codemp.intellij.jni.BufferHandler;
import com.codemp.intellij.jni.CodeMPHandler;
import com.codemp.intellij.task.TaskManager;
import com.codemp.intellij.util.ActionUtil; import com.codemp.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;
@ -21,7 +19,7 @@ public class BufferAttachAction extends AnAction {
//TODO "get" the Editor corresponding to buffer, for now use the current one //TODO "get" the Editor corresponding to buffer, for now use the current one
Editor editor = ActionUtil.getCurrentEditor(e); Editor editor = ActionUtil.getCurrentEditor(e);
TaskManager WorkspaceManager
.getOrCreateBufferTask(ActionUtil.getCurrentProject(e)) .getOrCreateBufferTask(ActionUtil.getCurrentProject(e))
.registerListener(bufferHandler, editor); .registerListener(bufferHandler, editor);
} }

View file

@ -1,7 +1,6 @@
package com.codemp.intellij.actions.buffer; package com.codemp.intellij.actions.buffer;
import com.codemp.intellij.CodeMP; import com.codemp.intellij.CodeMP;
import com.codemp.intellij.jni.CodeMPHandler;
import com.codemp.intellij.util.ActionUtil; import com.codemp.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;

View file

@ -1,7 +1,6 @@
package com.codemp.intellij.actions.buffer; package com.codemp.intellij.actions.buffer;
import com.codemp.intellij.CodeMP; import com.codemp.intellij.CodeMP;
import com.codemp.intellij.jni.CodeMPHandler;
import com.codemp.intellij.util.ActionUtil; import com.codemp.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;

View file

@ -2,9 +2,7 @@ package com.codemp.intellij.actions.buffer;
import com.codemp.intellij.CodeMP; import com.codemp.intellij.CodeMP;
import com.codemp.intellij.exceptions.ide.BufferDetachException; import com.codemp.intellij.exceptions.ide.BufferDetachException;
import com.codemp.intellij.jni.CodeMPHandler;
import com.codemp.intellij.task.BufferEventAwaiterTask; import com.codemp.intellij.task.BufferEventAwaiterTask;
import com.codemp.intellij.task.TaskManager;
import com.codemp.intellij.util.ActionUtil; import com.codemp.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;
@ -17,7 +15,6 @@ public class BufferDetachAction extends AnAction {
if(!res) throw new BufferDetachException(buffer); if(!res) throw new BufferDetachException(buffer);
CodeMP.ACTIVE_BUFFERS.remove(buffer); CodeMP.ACTIVE_BUFFERS.remove(buffer);
BufferEventAwaiterTask task = TaskManager.getBufferTask();
if(task != null) { if(task != null) {
task.unregisterListener(buffer); task.unregisterListener(buffer);
if(!silent) ActionUtil.notify(e, "Success", if(!silent) ActionUtil.notify(e, "Success",

View file

@ -1,15 +1,12 @@
package com.codemp.intellij.actions.buffer; package com.codemp.intellij.actions.buffer;
import com.codemp.intellij.CodeMP; import com.codemp.intellij.CodeMP;
import com.codemp.intellij.jni.CodeMPHandler;
import com.codemp.intellij.task.TaskManager;
import com.codemp.intellij.util.ActionUtil; import com.codemp.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.application.ApplicationManager; import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.ui.Messages;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class BufferSyncAction extends AnAction { public class BufferSyncAction extends AnAction {

View file

@ -1,47 +1,39 @@
package com.codemp.intellij.actions.workspace; package com.codemp.intellij.actions.workspace;
import com.codemp.intellij.CodeMP; import com.codemp.intellij.CodeMP;
import com.codemp.intellij.jni.CodeMPHandler;
import com.codemp.intellij.jni.CursorHandler;
import com.codemp.intellij.listeners.CursorEventListener;
import com.codemp.intellij.task.CursorEventAwaiterTask;
import com.codemp.intellij.task.TaskManager;
import com.codemp.intellij.util.ActionUtil; import com.codemp.intellij.util.ActionUtil;
import com.codemp.intellij.workspace.Workspace;
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.editor.EditorFactory;
import com.intellij.openapi.ui.Messages; import com.intellij.openapi.ui.Messages;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class WorkspaceJoinAction extends AnAction { public class WorkspaceJoinAction extends AnAction {
public static void join(AnActionEvent e, String workspace, boolean silent) { public static void join(AnActionEvent e, String workspaceId, boolean silent) {
CursorHandler cursorHandler = CodeMPHandler.join(workspace); CodeMP.ACTIVE_WORKSPACES.put(workspaceId, new Workspace(
workspaceId, CodeMP.getClient("join workspace"),
false, e.getProject() //TODO: implement remote projects
));
if(!silent) ActionUtil.notify(e, if(!silent) ActionUtil.notify(e,
"Success", String.format("Joined workspace %s!", workspace)); "Success", String.format("Joined workspace %s!", workspaceId));
CodeMP.LOGGER.debug("Joined workspace {}!", workspace); CodeMP.LOGGER.debug("Joined workspace {}!", workspaceId);
CursorEventAwaiterTask task = TaskManager
.getOrCreateCursorTask(ActionUtil.getCurrentProject(e), cursorHandler);
EditorFactory.getInstance()
.getEventMulticaster()
.addCaretListener(new CursorEventListener(cursorHandler), task);
} }
@Override @Override
public void actionPerformed(@NotNull AnActionEvent e) { public void actionPerformed(@NotNull AnActionEvent e) {
String session = Messages.showInputDialog( String workspaceId = Messages.showInputDialog(
"Session to connect to:", "Workspace to connect to:",
"CodeMP Join", "CodeMP Join",
Messages.getQuestionIcon()); Messages.getQuestionIcon());
try { try {
join(e, session, false); join(e, workspaceId, false);
} catch(Exception ex) { } catch(Exception ex) {
ActionUtil.notifyError(e, String.format( ActionUtil.notifyError(e, String.format(
"Failed to join session %s!", "Failed to join workspace %s!",
session), ex); workspaceId), ex);
} }
} }
} }

View file

@ -1,25 +1,33 @@
package com.codemp.intellij.actions.workspace; package com.codemp.intellij.actions.workspace;
import com.codemp.intellij.CodeMP; import com.codemp.intellij.CodeMP;
import com.codemp.intellij.jni.CodeMPHandler;
import com.codemp.intellij.util.ActionUtil; import com.codemp.intellij.util.ActionUtil;
import com.codemp.intellij.workspace.Workspace;
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 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, boolean silent) { public static void leave(AnActionEvent e, String workspaceId, boolean silent) {
CodeMPHandler.leaveWorkspace(); CodeMP.getClient("leave workspace")
.leaveWorkspace(workspaceId);
Disposer.dispose(CodeMP.ACTIVE_WORKSPACES.remove(workspaceId));
if(!silent) ActionUtil.notify(e, "Success", "Left workspace"); if(!silent) ActionUtil.notify(e, "Success", String.format("Left workspace %s!", workspaceId));
CodeMP.LOGGER.debug("Left workspace!"); CodeMP.LOGGER.debug("Left workspace!");
} }
@Override @Override
public void actionPerformed(@NotNull AnActionEvent e) { public void actionPerformed(@NotNull AnActionEvent e) {
String workspaceId = Messages.showInputDialog(
"Workspace to leave:",
"CodeMP Workspace Leave",
Messages.getQuestionIcon());
try { try {
leave(e, false); leave(e, workspaceId, false);
} catch(Exception ex) { } catch(Exception ex) {
ActionUtil.notifyError(e, "Failed to leave workspace!", ex); ActionUtil.notifyError(e, "Failed to leave workspace!", ex);
} }

View file

@ -0,0 +1,14 @@
package com.codemp.intellij.exceptions.ide;
import com.codemp.intellij.exceptions.CodeMPException;
/**
* Fired when trying to access the CodeMP client without first connecting
* to a server.
*/
public class NotConnectedException extends CodeMPException {
public NotConnectedException(String service) {
super(String.format("Failed to %s, you are not connected to a server!", service));
}
}

View file

@ -2,18 +2,23 @@ package com.codemp.intellij.listeners;
import com.codemp.intellij.CodeMP; import com.codemp.intellij.CodeMP;
import com.codemp.intellij.jni.CursorHandler; import com.codemp.intellij.jni.CursorHandler;
import com.codemp.intellij.task.CursorEventAwaiterTask;
import com.codemp.intellij.util.FileUtil;
import com.codemp.intellij.workspace.Workspace;
import com.intellij.openapi.editor.Caret; import com.intellij.openapi.editor.Caret;
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 com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
public class CursorEventListener implements CaretListener { public class CursorEventListener implements CaretListener {
private final CursorHandler cursorHandler; private final CursorHandler handler;
public CursorEventListener(CursorHandler cursorHandler) { public CursorEventListener(CursorHandler handler) {
this.cursorHandler = cursorHandler; this.handler = handler;
} }
@Override @Override
@ -26,8 +31,10 @@ public class CursorEventListener implements CaretListener {
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);
this.cursorHandler.send(
CodeMP.ACTIVE_BUFFERS_REVERSE.get(event.getEditor()), Editor editor = event.getEditor();
this.handler.send(
FileUtil.getRelativePath(editor.getProject(), editor.getVirtualFile()),
startPos.line, startPos.column, startPos.line, startPos.column,
endPos.line, endPos.column endPos.line, endPos.column
); );

View file

@ -0,0 +1,38 @@
package com.codemp.intellij.listeners;
import com.codemp.intellij.jni.WorkspaceHandler;
import com.codemp.intellij.task.BufferEventAwaiterTask;
import com.codemp.intellij.util.FileUtil;
import com.codemp.intellij.workspace.Workspace;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.FileEditorManagerListener;
import com.intellij.openapi.fileEditor.TextEditor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vfs.VirtualFile;
import org.jetbrains.annotations.NotNull;
public class WorkspaceFileClosedListener implements FileEditorManagerListener.Before {
private final WorkspaceHandler handler;
private final BufferEventAwaiterTask task;
public WorkspaceFileClosedListener(WorkspaceHandler 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);
}
}

View file

@ -0,0 +1,50 @@
package com.codemp.intellij.listeners;
import com.codemp.intellij.jni.BufferHandler;
import com.codemp.intellij.jni.WorkspaceHandler;
import com.codemp.intellij.task.BufferEventAwaiterTask;
import com.codemp.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 org.jetbrains.annotations.NotNull;
import java.util.List;
public class WorkspaceFileOpenedListener implements FileOpenedSyncListener {
private final WorkspaceHandler handler;
private final BufferEventAwaiterTask task;
public WorkspaceFileOpenedListener(WorkspaceHandler 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;
BufferHandler bufferHandler = this.handler.attachToBuffer(path);
Disposable disp = Disposer.newDisposable(String.format("codemp-buffer-%s", path));
editor.getDocument().addDocumentListener(new BufferEventListener(bufferHandler), disp);
editor.getDocument().setText(""); //empty it so we can start receiving
this.task.activeBuffers.put(path, disp);
});
}
}

View file

@ -4,10 +4,10 @@ import com.codemp.intellij.CodeMP;
import com.codemp.intellij.exceptions.lib.ChannelException; import com.codemp.intellij.exceptions.lib.ChannelException;
import com.codemp.intellij.exceptions.lib.DeadlockedException; import com.codemp.intellij.exceptions.lib.DeadlockedException;
import com.codemp.intellij.jni.BufferHandler; import com.codemp.intellij.jni.BufferHandler;
import com.codemp.intellij.jni.CodeMPHandler;
import com.codemp.intellij.jni.StringVec; import com.codemp.intellij.jni.StringVec;
import com.codemp.intellij.jni.TextChangeWrapper; import com.codemp.intellij.jni.TextChangeWrapper;
import com.codemp.intellij.listeners.BufferEventListener; import com.codemp.intellij.jni.WorkspaceHandler;
import com.codemp.intellij.util.FileUtil;
import com.intellij.openapi.Disposable; import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor; import com.intellij.openapi.command.CommandProcessor;
@ -22,44 +22,23 @@ import java.util.*;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
public class BufferEventAwaiterTask extends Task.Backgroundable implements Disposable { public class BufferEventAwaiterTask extends Task.Backgroundable implements Disposable {
private final Map<String, Disposable> bufferListeners = new ConcurrentHashMap<>(); public final Map<String, Disposable> activeBuffers;
private final WorkspaceHandler handler;
public BufferEventAwaiterTask(@NotNull Project project) { public BufferEventAwaiterTask(@NotNull Project project, @NotNull WorkspaceHandler handler) {
super(project, "Awaiting CodeMP buffer events", false); super(project, "Awaiting CodeMP buffer events", false);
} this.activeBuffers = new ConcurrentHashMap<>();
this.handler = handler;
public void registerListener(BufferHandler handler, Editor editor) {
CodeMP.ACTIVE_BUFFERS.put(handler.getName(), editor); //mark as active
CodeMP.ACTIVE_BUFFERS_REVERSE.put(editor, handler.getName());
Disposable disposable = Disposer
.newDisposable(this, String.format("codemp-buffer-%s", handler.getName()));
editor.getDocument()
.addDocumentListener(new BufferEventListener(handler), disposable);
bufferListeners.put(handler.getName(), disposable);
}
public void unregisterListener(String name) {
CodeMP.ACTIVE_BUFFERS_REVERSE.remove(CodeMP.ACTIVE_BUFFERS.remove(name));
Disposable listener = this.bufferListeners.remove(name);
if(listener != null)
listener.dispose();
} }
@Override @Override
public void dispose() {} @SuppressWarnings("InfiniteLoopStatement")
@Override
@SuppressWarnings({"InfiniteLoopStatement", "UnstableApiUsage"})
public void run(@NotNull ProgressIndicator indicator) { public void run(@NotNull ProgressIndicator indicator) {
try { try {
while(true) { while(true) {
StringVec buffers = new StringVec(); //jni moment StringVec buffers = new StringVec(); //jni moment
CodeMP.ACTIVE_BUFFERS.keySet().forEach(buffers::push); this.activeBuffers.keySet().forEach(buffers::push);
Optional<BufferHandler> bufferOptional = CodeMPHandler.selectBuffer(buffers, 100L); Optional<BufferHandler> bufferOptional = this.handler.selectBuffer(buffers, 100L);
if(bufferOptional.isEmpty()) if(bufferOptional.isEmpty())
continue; continue;
BufferHandler buffer = bufferOptional.get(); BufferHandler buffer = bufferOptional.get();
@ -81,7 +60,7 @@ public class BufferEventAwaiterTask extends Task.Backgroundable implements Dispo
changeList.add(change); changeList.add(change);
} }
Editor bufferEditor = CodeMP.ACTIVE_BUFFERS.get(buffer.getName()); Editor bufferEditor = FileUtil.getActiveEditorByPath(this.myProject, buffer.getName());
ApplicationManager.getApplication().invokeLaterOnWriteThread(() -> ApplicationManager.getApplication().invokeLaterOnWriteThread(() ->
ApplicationManager.getApplication().runWriteAction(() -> ApplicationManager.getApplication().runWriteAction(() ->
CommandProcessor.getInstance().executeCommand( CommandProcessor.getInstance().executeCommand(
@ -96,8 +75,14 @@ public class BufferEventAwaiterTask extends Task.Backgroundable implements Dispo
))); )));
} }
} catch(ChannelException ex) { //exited } catch(ChannelException ex) { //exited
TaskManager.nullBufferTask(); //TODO handle stop
Disposer.dispose(this); //stopped Disposer.dispose(this); //stopped
} }
} }
@Override
public void dispose() {
this.activeBuffers.values().forEach(Disposable::dispose);
this.activeBuffers.clear();
}
} }

View file

@ -5,6 +5,7 @@ import com.codemp.intellij.exceptions.lib.ChannelException;
import com.codemp.intellij.jni.CursorEventWrapper; import com.codemp.intellij.jni.CursorEventWrapper;
import com.codemp.intellij.jni.CursorHandler; import com.codemp.intellij.jni.CursorHandler;
import com.codemp.intellij.util.ColorUtil; import com.codemp.intellij.util.ColorUtil;
import com.codemp.intellij.util.FileUtil;
import com.intellij.openapi.Disposable; import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.Editor;
@ -15,7 +16,6 @@ import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task; import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Disposer;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.awt.*; import java.awt.*;
@ -33,17 +33,13 @@ public class CursorEventAwaiterTask extends Task.Backgroundable implements Dispo
this.handler = handler; this.handler = handler;
} }
@Override
public void dispose() {}
@Override @Override
@SuppressWarnings("InfiniteLoopStatement") @SuppressWarnings("InfiniteLoopStatement")
public void run(@NotNull ProgressIndicator indicator) { public void run(@NotNull ProgressIndicator indicator) {
assert myProject != null; //will never fail
try { try {
while(true) { while(true) {
CursorEventWrapper event = handler.recv(); CursorEventWrapper event = this.handler.recv();
Editor editor = CodeMP.ACTIVE_BUFFERS.get(event.getBuffer()); Editor editor = FileUtil.getActiveEditorByPath(this.myProject, event.getBuffer());
if(editor == null) if(editor == null)
continue; continue;
@ -77,7 +73,7 @@ public class CursorEventAwaiterTask extends Task.Backgroundable implements Dispo
HighlighterLayer.SELECTION, HighlighterLayer.SELECTION,
new TextAttributes( new TextAttributes(
null, null,
ColorUtil.colorFromUsername(event.getUser()), ColorUtil.hashColor(event.getUser()),
null, null,
null, null,
Font.PLAIN Font.PLAIN
@ -90,9 +86,12 @@ public class CursorEventAwaiterTask extends Task.Backgroundable implements Dispo
} catch(IndexOutOfBoundsException ignored) {} } catch(IndexOutOfBoundsException ignored) {}
} }
} catch(ChannelException ex) { //exited } catch(ChannelException ex) { //exited
this.highlighterMap.forEach((s, r) -> r.dispose()); this.run(indicator);
TaskManager.nullCursorTask();
Disposer.dispose(this);
} }
} }
@Override
public void dispose() {
this.highlighterMap.forEach((s, r) -> r.dispose());
}
} }

View file

@ -1,40 +0,0 @@
package com.codemp.intellij.task;
import com.codemp.intellij.jni.CursorHandler;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
public class TaskManager {
private static CursorEventAwaiterTask cursorTask = null;
//TODO in the future joining a workspace will give you a project matching remote
public static CursorEventAwaiterTask getOrCreateCursorTask(Project project, CursorHandler handler) {
if(cursorTask != null)
return cursorTask;
cursorTask = new CursorEventAwaiterTask(project, handler);
ProgressManager.getInstance().run(cursorTask);
return cursorTask;
}
public static void nullCursorTask() {
cursorTask = null;
}
private static BufferEventAwaiterTask bufferTask = null;
public static BufferEventAwaiterTask getBufferTask() {
return bufferTask;
}
public static BufferEventAwaiterTask getOrCreateBufferTask(Project project) {
if(bufferTask != null)
return bufferTask;
bufferTask = new BufferEventAwaiterTask(project);
ProgressManager.getInstance().run(bufferTask);
return bufferTask;
}
public static void nullBufferTask() {
bufferTask = null;
}
}

View file

@ -5,12 +5,9 @@ import com.intellij.ui.JBColor;
import java.awt.Color; import java.awt.Color;
public class ColorUtil { public class ColorUtil {
public static JBColor colorFromUsername(String username) { public static JBColor hashColor(String username) {
int hash = username.hashCode(); int hash = username.hashCode();
@SuppressWarnings("all")
Color hashColor = new Color(hash | (0xFF << 24)); Color hashColor = new Color(hash | (0xFF << 24));
return new JBColor(hashColor, hashColor.darker()); return new JBColor(hashColor, hashColor.darker());
} }
} }

View file

@ -0,0 +1,34 @@
package com.codemp.intellij.util;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.TextEditor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import java.util.Arrays;
public class FileUtil {
public static String getRelativePath(Project project, VirtualFile vf) {
if(ProjectRootManager.getInstance(project).getFileIndex().isInContent(vf))
return Arrays.stream(ProjectRootManager.getInstance(project).getContentRoots())
.filter(r -> VfsUtilCore.isAncestor(r, vf, false))
.findAny()
.map(root -> VfsUtilCore.getRelativePath(vf, root))
.orElse(null);
return null;
}
public static Editor getActiveEditorByPath(Project project, String path) {
return Arrays.stream(FileEditorManager.getInstance(project).getAllEditors())
.filter(fe -> fe instanceof TextEditor)
.map(fe -> {
TextEditor te = (TextEditor) fe;
return te.getEditor();
}).filter(editor -> path.equals(FileUtil.getRelativePath(editor.getProject(), editor.getVirtualFile())))
.findFirst()
.orElse(null);
}
}

View file

@ -0,0 +1,60 @@
package com.codemp.intellij.workspace;
import com.codemp.intellij.jni.ClientHandler;
import com.codemp.intellij.jni.WorkspaceHandler;
import com.codemp.intellij.listeners.*;
import com.codemp.intellij.task.BufferEventAwaiterTask;
import com.codemp.intellij.task.CursorEventAwaiterTask;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.fileEditor.FileEditorManagerListener;
import com.intellij.openapi.fileEditor.FileOpenedSyncListener;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.project.Project;
import com.intellij.util.messages.MessageBusConnection;
public class Workspace implements Disposable {
public final String id;
public final String url;
public final boolean isRemote;
public final WorkspaceHandler handler;
public final Project project;
public final BufferEventAwaiterTask bufferTask;
public final CursorEventAwaiterTask cursorTask;
/**
* The constructor, that will also take care of creating the tasks and listeners associated with it.
* @param id unique id of the workspace on the server
* @param client the {@link ClientHandler} to use
* @param isRemote whether the project is remote
* @param project the {@link Project} to use
*/
public Workspace(String id, ClientHandler client, boolean isRemote, Project project) {
this.id = id;
this.url = client.getUrl();
this.isRemote = isRemote;
this.handler = client.joinWorkspace(id);
this.project = project;
this.cursorTask = new CursorEventAwaiterTask(project, this.handler.getCursor());
ProgressManager.getInstance().run(this.cursorTask);
this.bufferTask = new BufferEventAwaiterTask(project, this.handler);
ProgressManager.getInstance().run(this.bufferTask);
// buffer listening
MessageBusConnection conn = this.project.getMessageBus().connect(this);
conn.subscribe(FileOpenedSyncListener.TOPIC,
new WorkspaceFileOpenedListener(this.handler, this.bufferTask));
conn.subscribe(FileEditorManagerListener.Before.FILE_EDITOR_MANAGER,
new WorkspaceFileClosedListener(this.handler, this.bufferTask));
// cursor listening
EditorFactory.getInstance()
.getEventMulticaster()
.addCaretListener(new CursorEventListener(this.handler.getCursor()), this.cursorTask);
}
@Override
public void dispose() {}
}

View file

@ -12,6 +12,7 @@
<add-to-group group-id="ToolsMenu" anchor="first"/> <add-to-group group-id="ToolsMenu" anchor="first"/>
<action id="codemp.fast-forward" class="com.codemp.intellij.actions.FastForwardAction" text="Just Hurry"/> <action id="codemp.fast-forward" class="com.codemp.intellij.actions.FastForwardAction" text="Just Hurry"/>
<action id="codemp.connect" class="com.codemp.intellij.actions.ConnectAction" text="Connect..."/> <action id="codemp.connect" class="com.codemp.intellij.actions.ConnectAction" text="Connect..."/>
<action id="codemp.connect" class="com.codemp.intellij.actions.DisconnectAction" text="Disconnect"/>
<group id="codemp.workspace" text="Workspace" popup="true"> <group id="codemp.workspace" text="Workspace" popup="true">
<action id="codemp.workspace.join" class="com.codemp.intellij.actions.workspace.WorkspaceJoinAction" <action id="codemp.workspace.join" class="com.codemp.intellij.actions.workspace.WorkspaceJoinAction"

View file

@ -19,16 +19,16 @@ lazy_static! {
#[generate_interface_doc] #[generate_interface_doc]
/// the handler class that represent an instance of a CodeMP client /// the handler class that represent an instance of a CodeMP client
struct CodeMPHandler { struct ClientHandler {
client: CodempClient, client: CodempClient,
url: String url: String
} }
impl CodeMPHandler { impl ClientHandler {
#[generate_interface(constructor)] #[generate_interface(constructor)]
/// constructor required by flapigen, DO NOT CALL THIS /// constructor required by flapigen, DO NOT CALL THIS
fn new(address: &str) -> CodeMPHandler { fn new(address: &str) -> ClientHandler {
CodeMPHandler { ClientHandler {
client: RT.block_on(CodempClient::new(address)).unwrap(), client: RT.block_on(CodempClient::new(address)).unwrap(),
url: address.to_string() url: address.to_string()
} }
@ -83,7 +83,7 @@ impl WorkspaceHandler {
#[generate_interface] #[generate_interface]
/// attach to a buffer and get a [crate::BufferHandler] for it /// attach to a buffer and get a [crate::BufferHandler] for it
fn attach_buffer(&mut self, path: &str) -> CodempResult<BufferHandler> { fn attach_to_buffer(&mut self, path: &str) -> CodempResult<BufferHandler> {
RT.block_on(RT.block_on(self.workspace.write()).attach(path)) RT.block_on(RT.block_on(self.workspace.write()).attach(path))
.map(|buffer| BufferHandler { buffer }) .map(|buffer| BufferHandler { buffer })
} }
@ -124,7 +124,7 @@ impl WorkspaceHandler {
#[generate_interface] #[generate_interface]
/// detach from a buffer /// detach from a buffer
fn detach_buffer(&mut self, path: &str) -> bool { fn detach_from_buffer(&mut self, path: &str) -> bool {
RT.block_on(self.workspace.write()).detach(path) RT.block_on(self.workspace.write()).detach(path)
} }