feat: buffer and workspace disconnect, lifetime system for listeners

This commit is contained in:
zaaarf 2023-11-17 13:03:37 +01:00
parent dfae6d6b9d
commit 8c04d9b8c2
No known key found for this signature in database
GPG key ID: 82240E075E31FA4C
10 changed files with 210 additions and 40 deletions

View file

@ -1,6 +1,7 @@
package com.codemp.intellij.actions; package com.codemp.intellij.actions;
import com.codemp.intellij.actions.buffer.BufferAttachAction; import com.codemp.intellij.actions.buffer.BufferAttachAction;
import com.codemp.intellij.actions.workspace.WorkspaceJoinAction;
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 org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -13,7 +14,7 @@ public class FastForwardAction extends AnAction {
public void actionPerformed(@NotNull AnActionEvent e) { public void actionPerformed(@NotNull AnActionEvent e) {
try { try {
ConnectAction.connect("http://alemi.dev:50052", true); ConnectAction.connect("http://alemi.dev:50052", true);
JoinAction.join(e, "default", true); WorkspaceJoinAction.join(e, "default", true);
BufferAttachAction.attach(e, "test", true); BufferAttachAction.attach(e, "test", true);
} catch(Exception ex) { } catch(Exception ex) {
throw new RuntimeException(ex); throw new RuntimeException(ex);

View file

@ -6,6 +6,7 @@ import com.codemp.intellij.jni.CodeMPHandler;
import com.codemp.intellij.jni.TextChangeWrapper; import com.codemp.intellij.jni.TextChangeWrapper;
import com.codemp.intellij.listeners.BufferEventListener; import com.codemp.intellij.listeners.BufferEventListener;
import com.codemp.intellij.util.ActionUtil; import com.codemp.intellij.util.ActionUtil;
import com.codemp.intellij.util.DisposableRegistry;
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;
@ -32,12 +33,18 @@ public class BufferAttachAction extends AnAction {
Project project = ActionUtil.getCurrentProject(e); Project project = ActionUtil.getCurrentProject(e);
Document document = ActionUtil.getCurrentEditor(e).getDocument(); Document document = ActionUtil.getCurrentEditor(e).getDocument();
document.addDocumentListener(listener); document.addDocumentListener(listener, DisposableRegistry.getOrCreate(String.format("codemp-buffer-%s", buffer)));
ProgressManager.getInstance().run(new Task.Backgroundable(e.getProject(), "Awaiting CodeMP buffer events") { ProgressManager.getInstance().run(new Task.Backgroundable(e.getProject(), "Awaiting CodeMP buffer events") {
@Override @Override
@SuppressWarnings({"InfiniteLoopStatement", "UnstableApiUsage"}) @SuppressWarnings({"InfiniteLoopStatement", "UnstableApiUsage"})
public void run(@NotNull ProgressIndicator indicator) { public void run(@NotNull ProgressIndicator indicator) {
try {
Thread.sleep(100); //tonioware
} catch(InterruptedException ex) {
throw new RuntimeException(ex);
}
while(true) { while(true) {
try { try {
TextChangeWrapper event = bufferHandler.recv(); TextChangeWrapper event = bufferHandler.recv();

View file

@ -0,0 +1,40 @@
package com.codemp.intellij.actions.buffer;
import com.codemp.intellij.CodeMP;
import com.codemp.intellij.exceptions.ide.BufferDetachException;
import com.codemp.intellij.jni.CodeMPHandler;
import com.codemp.intellij.util.DisposableRegistry;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.ui.Messages;
import org.jetbrains.annotations.NotNull;
public class BufferDetachAction extends AnAction {
public static void detach(String buffer, boolean silent) throws Exception {
boolean res = CodeMPHandler.detach(buffer);
if(!res) throw new BufferDetachException(buffer);
//dispose of listener's associated disposable
DisposableRegistry.disposeOf(String.format("codemp-buffer-%s", buffer));
if(!silent) Messages.showInfoMessage(String.format("Detached from buffer %s!", buffer),
"Detach from CodeMP Buffer" );
CodeMP.LOGGER.debug("Detached from buffer {}!", buffer);
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
String buffer = Messages.showInputDialog(
"Buffer name:",
"Detach from CodeMP Buffer",
Messages.getQuestionIcon());
try {
detach(buffer, false);
} catch(Exception ex) {
Messages.showErrorDialog(String.format(
"Failed to detach from buffer with name %s: %s!",
buffer, ex.getMessage()), "Detach from CodeMP Buffer");
}
}
}

View file

@ -1,4 +1,4 @@
package com.codemp.intellij.actions; 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.CodeMPHandler;
@ -7,7 +7,7 @@ import com.codemp.intellij.jni.CursorHandler;
import com.codemp.intellij.listeners.CursorEventListener; import com.codemp.intellij.listeners.CursorEventListener;
import com.codemp.intellij.util.ActionUtil; import com.codemp.intellij.util.ActionUtil;
import com.codemp.intellij.util.ColorUtil; import com.codemp.intellij.util.ColorUtil;
import com.intellij.openapi.Disposable; import com.codemp.intellij.util.DisposableRegistry;
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;
@ -25,18 +25,18 @@ import com.intellij.openapi.ui.Messages;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.awt.*; import java.awt.*;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class JoinAction extends AnAction { public class WorkspaceJoinAction extends AnAction {
private static final Map<String, RangeHighlighter> highlighterMap = new HashMap<>(); private static final Map<String, RangeHighlighter> highlighterMap = new ConcurrentHashMap<>();
public static void join(AnActionEvent e, String session, boolean silent) throws Exception { public static void join(AnActionEvent e, String workspace, boolean silent) throws Exception {
CursorHandler cursorHandler = CodeMPHandler.join(session); CursorHandler cursorHandler = CodeMPHandler.join(workspace);
if(!silent) Messages.showInfoMessage(String.format("Joined session %s!", session), "CodeMP"); if(!silent) Messages.showInfoMessage(String.format("Joined workspace %s!", workspace), "CodeMP");
else CodeMP.LOGGER.debug("Joined session {}!", session); else CodeMP.LOGGER.debug("Joined workspace {}!", workspace);
Editor editor = ActionUtil.getCurrentEditor(e); Editor editor = ActionUtil.getCurrentEditor(e);
@ -49,7 +49,8 @@ public class JoinAction extends AnAction {
EditorFactory.getInstance() EditorFactory.getInstance()
.getEventMulticaster() .getEventMulticaster()
.addCaretListener(new CursorEventListener(), task); .addCaretListener(new CursorEventListener(),
DisposableRegistry.getOrCreate(String.format("codemp-cursor-%s", workspace)));
ProgressManager.getInstance().run(task); ProgressManager.getInstance().run(task);
} }
@ -74,7 +75,7 @@ public class JoinAction extends AnAction {
//TODO this is janky as it shows a progress bar it doesn't use tbh //TODO this is janky as it shows a progress bar it doesn't use tbh
//implements disposable so i can use it as lifetime ig //implements disposable so i can use it as lifetime ig
private static class CursorEventAwaiter extends Task.Backgroundable implements Disposable { private static class CursorEventAwaiter extends Task.Backgroundable {
private final CursorHandler handler; private final CursorHandler handler;
private final Editor editor; private final Editor editor;
@ -85,9 +86,6 @@ public class JoinAction extends AnAction {
this.editor = editor; this.editor = editor;
} }
@Override
public void dispose() {}
@Override @Override
@SuppressWarnings("InfiniteLoopStatement") @SuppressWarnings("InfiniteLoopStatement")
public void run(@NotNull ProgressIndicator indicator) { public void run(@NotNull ProgressIndicator indicator) {

View file

@ -0,0 +1,33 @@
package com.codemp.intellij.actions.workspace;
import com.codemp.intellij.CodeMP;
import com.codemp.intellij.jni.CodeMPHandler;
import com.codemp.intellij.util.DisposableRegistry;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.ui.Messages;
import org.jetbrains.annotations.NotNull;
public class WorkspaceLeaveAction extends AnAction {
public static void leave(boolean silent) throws Exception {
CodeMPHandler.leaveWorkspace();
//dispose of listener's associated disposable
DisposableRegistry.disposeOf("codemp-cursor");
if(!silent) Messages.showInfoMessage("Left workspace!",
"Detach from CodeMP Buffer" );
CodeMP.LOGGER.debug("Left workspace!");
}
@Override
public void actionPerformed(@NotNull AnActionEvent e) {
try {
leave(false);
} catch(Exception ex) {
Messages.showErrorDialog(String.format(
"Failed to leave workspace: %s!",
ex.getMessage()), "Leave CodeMP Workspace");
}
}
}

View file

@ -0,0 +1,13 @@
package com.codemp.intellij.exceptions.ide;
import com.codemp.intellij.exceptions.CodeMPException;
/**
* Thrown upon failure to detach from a buffer.
*/
public class BufferDetachException extends CodeMPException {
public BufferDetachException(String name) {
super(String.format("Could not detach from buffer named \"%s\"!", name));
}
}

View file

@ -0,0 +1,66 @@
package com.codemp.intellij.util;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.util.Disposer;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* A registry holding {@link Disposable Disposables} used within the plugin,
* since there's no other way to keep track of them across different sections.
* Only parentless {@link Disposable Disposables} are handled here, since
* those with a parent will be disposed automatically.
*/
public class DisposableRegistry {
private static final Map<String, Disposable> DISPOSABLE_MAP = new ConcurrentHashMap<>();
public static boolean exists(String name) {
return DISPOSABLE_MAP.containsKey(name);
}
public static Disposable get(String name) {
return DISPOSABLE_MAP.get(name);
}
public static Disposable create(String name) {
disposeOf(name); //get rid of existing ones, if there is one, to prevent memory leaks
Disposable res = Disposer.newDisposable(name);
DISPOSABLE_MAP.put(name, res);
return res;
}
public static Disposable getOrCreate(String name) {
Disposable disposable = DISPOSABLE_MAP.get(name);
if(disposable == null)
disposable = create(name);
return disposable;
}
public static boolean track(String name, Disposable disposable) {
boolean replaced = exists(name);
if(replaced)
disposeOf(name);
DISPOSABLE_MAP.put(name, disposable);
return replaced;
}
public static boolean disposeOf(String name) {
if(exists(name)) {
Disposable disposable = DISPOSABLE_MAP.remove(name);
Disposer.dispose(disposable);
return true;
} else return false;
}
public static boolean disposeOf(Disposable disposable) {
if(DISPOSABLE_MAP.containsValue(disposable)) {
return DISPOSABLE_MAP.entrySet().removeIf(entry -> {
if(entry.getValue().equals(disposable)) {
Disposer.dispose(disposable, false);
return true;
} else return false;
});
} else return false;
}
}

View file

@ -27,16 +27,24 @@
<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.join" class="com.codemp.intellij.actions.JoinAction" text="Join..."/>
<group id="codemp.buffer" text="Buffer" popup="true"> <group id="codemp.workspace" text="Workspace" popup="true">
<action id="codemp.buffer.create" class="com.codemp.intellij.actions.buffer.BufferCreateAction" <action id="codemp.workspace.join" class="com.codemp.intellij.actions.workspace.WorkspaceJoinAction"
text="Create Buffer"/> text="Join Workspace"/>
<action id="codemp.buffer.create-with-content" <action id="codemp.workspace.leave" class="com.codemp.intellij.actions.workspace.WorkspaceLeaveAction"
class="com.codemp.intellij.actions.buffer.BufferCreateWithContentAction" text="Leave Workspace"/>
text="Create Buffer with Content"/> </group>
<action id="codemp.buffer.attach" class="com.codemp.intellij.actions.buffer.BufferAttachAction" <group id="codemp.buffer" text="Buffer" popup="true">
text="Attach to Buffer"/> <action id="codemp.buffer.create" class="com.codemp.intellij.actions.buffer.BufferCreateAction"
</group> text="Create Buffer"/>
<action id="codemp.buffer.create-with-content"
class="com.codemp.intellij.actions.buffer.BufferCreateWithContentAction"
text="Create Buffer with Content"/>
<action id="codemp.buffer.attach" class="com.codemp.intellij.actions.buffer.BufferAttachAction"
text="Attach to Buffer"/>
<action id="codemp.buffer.detach" class="com.codemp.intellij.actions.buffer.BufferDetachAction"
text="Detach from Buffer"/>
</group>
</group> </group>
</actions> </actions>
@ -44,4 +52,4 @@
<!-- Extension points defined by the plugin. <!-- Extension points defined by the plugin.
Read more: https://plugins.jetbrains.com/docs/intellij/plugin-extension-points.html --> Read more: https://plugins.jetbrains.com/docs/intellij/plugin-extension-points.html -->
</extensions> </extensions>
</idea-plugin> </idea-plugin>

View file

@ -1,3 +1,4 @@
use codemp::Error;
use codemp::prelude::CodempError; use codemp::prelude::CodempError;
pub struct ErrorWrapper(CodempError); pub struct ErrorWrapper(CodempError);
@ -15,6 +16,7 @@ impl ErrorWrapper {
format!("Error {}: {}", status, message), format!("Error {}: {}", status, message),
CodempError::InvalidState { msg } => msg.to_string(), CodempError::InvalidState { msg } => msg.to_string(),
CodempError::Filler { message } => message.to_string(), CodempError::Filler { message } => message.to_string(),
Error::Deadlocked => { "Error: deadlocked! (safe to retry)".to_string() }
CodempError::Channel { send } => { CodempError::Channel { send } => {
if *send { if *send {
"Error while sending message on channel: the channel was closed!".to_string() "Error while sending message on channel: the channel was closed!".to_string()
@ -22,6 +24,6 @@ impl ErrorWrapper {
"Error while reading message from channel: the channel was closed!".to_string() "Error while reading message from channel: the channel was closed!".to_string()
} }
} }
} }
} }
} }

View file

@ -43,6 +43,11 @@ impl CodeMPHandler {
convert_buffer(CODEMP_INSTANCE.attach(&path)) convert_buffer(CODEMP_INSTANCE.attach(&path))
} }
#[generate_interface]
fn detach(path: String) -> Result<bool, String> {
convert(CODEMP_INSTANCE.disconnect_buffer(&path))
}
#[generate_interface] #[generate_interface]
fn get_cursor() -> Result<CursorHandler, String> { fn get_cursor() -> Result<CursorHandler, String> {
convert_cursor(CODEMP_INSTANCE.get_cursor()) convert_cursor(CODEMP_INSTANCE.get_cursor())
@ -154,8 +159,8 @@ impl CursorHandler {
fn send(&self, buffer: String, start_row: i32, start_col: i32, end_row: i32, end_col: i32) -> Result<(), String> { fn send(&self, buffer: String, start_row: i32, start_col: i32, end_row: i32, end_col: i32) -> Result<(), String> {
self.cursor.send(CodempCursorPosition { self.cursor.send(CodempCursorPosition {
buffer, buffer,
start: Some(CodempRowCol { row: start_row, col: start_col }), start: CodempRowCol::wrap(start_row, start_col),
end: Some(CodempRowCol { row: end_row, col: end_col }) end: CodempRowCol::wrap(end_row, end_col)
}).map_err(|err| ErrorWrapper::from(err).get_error_message()) }).map_err(|err| ErrorWrapper::from(err).get_error_message())
} }
} }
@ -205,20 +210,17 @@ impl BufferHandler {
fn recv(&self) -> Result<TextChangeWrapper, String> { fn recv(&self) -> Result<TextChangeWrapper, String> {
match self.buffer.blocking_recv(CODEMP_INSTANCE.rt()) { match self.buffer.blocking_recv(CODEMP_INSTANCE.rt()) {
Err(err) => Err(ErrorWrapper::from(err).get_error_message()), Err(err) => Err(ErrorWrapper::from(err).get_error_message()),
Ok(change) => { Ok(change) => Ok(TextChangeWrapper {
println!("test {:?}", change); start: change.span.start,
Ok(TextChangeWrapper { end: change.span.end,
start: change.span.start, content: change.content.clone()
end: change.span.end, })
content: change.content.clone()
})
}
} }
} }
#[generate_interface] #[generate_interface]
fn send(&self, start_offset: usize, end_offset: usize, content: String) -> Result<(), String> { fn send(&self, start_offset: usize, end_offset: usize, content: String) -> Result<(), String> {
self.buffer.send(CodempTextChange { span: start_offset..end_offset, content, after: "".to_string() }) self.buffer.send(CodempTextChange { span: start_offset..end_offset, content })
.map_err(|err| ErrorWrapper::from(err).get_error_message()) .map_err(|err| ErrorWrapper::from(err).get_error_message())
} }
} }