feat: memory management + create buffer (ux needs improvement)

This commit is contained in:
zaaarf 2024-09-25 23:53:00 +02:00
parent 13c3601e90
commit fe46d6bfd3
No known key found for this signature in database
GPG key ID: 102E445F4C3F829B
8 changed files with 195 additions and 109 deletions

View file

@ -37,8 +37,6 @@ public class CodeMPSettings implements PersistentStateComponent<CodeMPSettings.S
@Getter
@Setter
public static class State {
String serverUrl;
private static CredentialAttributes createCredentialAttributes() {
return new CredentialAttributes(CredentialAttributesKt.generateServiceName(
"CodeMP",

View file

@ -24,11 +24,6 @@ final class CodeMPSettingsConfigurable implements Configurable {
return "CodeMP";
}
@Override
public JComponent getPreferredFocusedComponent() {
return this.component.serverUrlField;
}
@Nullable
@Override
public JComponent createComponent() {
@ -40,8 +35,7 @@ final class CodeMPSettingsConfigurable implements Configurable {
public boolean isModified() {
CodeMPSettings.State state = Objects.requireNonNull(CodeMPSettings.getInstance().getState());
Credentials creds = state.getCredentials();
return !this.component.serverUrlField.getText().equals(state.serverUrl)
|| (creds == null && (this.component.userNameField.getText() != null || this.component.passwordField.getPassword() != null))
return (creds == null && (this.component.userNameField.getText() != null || this.component.passwordField.getPassword() != null))
|| creds != null && (
!Objects.equals(creds.getUserName(), this.component.userNameField.getText())
|| !Objects.equals(creds.getPassword(), new OneTimeString(this.component.passwordField.getPassword()))
@ -51,7 +45,6 @@ final class CodeMPSettingsConfigurable implements Configurable {
@Override
public void apply() {
CodeMPSettings.State state = Objects.requireNonNull(CodeMPSettings.getInstance().getState());
state.serverUrl = this.component.serverUrlField.getText();
state.setCredentials(new Credentials(
this.component.userNameField.getText(),
this.component.passwordField.getPassword()
@ -61,8 +54,6 @@ final class CodeMPSettingsConfigurable implements Configurable {
@Override
public void reset() {
CodeMPSettings.State state = Objects.requireNonNull(CodeMPSettings.getInstance().getState());
this.component.serverUrlField.setText(state.serverUrl);
Credentials cred = state.getCredentials();
if(cred != null) {
this.component.userNameField.setText(cred.getUserName());
@ -77,14 +68,12 @@ final class CodeMPSettingsConfigurable implements Configurable {
private static class Component {
final JPanel mainPanel;
final JBTextField serverUrlField = new JBTextField();
final JBTextField userNameField = new JBTextField();
final JBPasswordField passwordField = new JBPasswordField();
Component() {
this.mainPanel = FormBuilder.createFormBuilder()
.addComponent(new JBLabel("Connection").withFont(JBFont.h2().asBold()))
.addLabeledComponent(new JBLabel("Server address:"), this.serverUrlField, 1, false)
.addLabeledComponent(new JBLabel("Username:"), this.userNameField, 1, false)
.addLabeledComponent(new JBLabel("Password:"), this.passwordField, 1, false)
.addComponentFillVertically(new JPanel(), 0)

View file

@ -9,6 +9,7 @@ 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 com.jgoodies.forms.layout.FormLayout;
import mp.code.intellij.CodeMP;
import mp.code.intellij.util.cb.BufferCallback;
import mp.code.intellij.util.FileUtil;
@ -23,7 +24,7 @@ import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.util.Optional;
public class CodeMPToolWindowFactory implements ToolWindowFactory, DumbAware {
public class CodeMPWindowFactory implements ToolWindowFactory, DumbAware {
@Override
public void createToolWindowContent(
@NotNull Project project,
@ -111,6 +112,7 @@ public class CodeMPToolWindowFactory implements ToolWindowFactory, DumbAware {
CodeMPToolWindow.this.redraw(project);
}
});
createButton.setSize(createButton.getPreferredSize());
JTree tree = drawTree(CodeMP.getActiveWorkspace().getFileTree(Optional.empty(), false));
tree.addMouseListener(new SimpleMouseListener() {
@ -138,6 +140,7 @@ public class CodeMPToolWindowFactory implements ToolWindowFactory, DumbAware {
});
}
});
this.add(createButton);
this.add(tree);
}

View file

@ -4,14 +4,9 @@ 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.Disposable;
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;
@ -19,18 +14,16 @@ 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 mp.code.intellij.util.cb.CursorCallback;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.awt.*;
import java.util.Objects;
import java.util.Optional;
@ -51,14 +44,11 @@ public class InteractionUtil {
Objects.requireNonNull(credentials.getUserName()),
Objects.requireNonNull(credentials.getPasswordAsString())
);
MemoryManager.startClientLifetime();
if(after != null) after.run();
notifyInfo(
project,
"Success",
String.format("Connected to %s!", state.getServerUrl())
);
notifyInfo(project, "Success", "Connected to server!");
} catch(NullPointerException e) {
Notifications.Bus.notify(new Notification(
"CodeMP",
@ -75,6 +65,7 @@ public class InteractionUtil {
public static void disconnect(@Nullable Project project) {
CodeMP.disconnect();
MemoryManager.endClientLifetime();
notifyInfo(project, "Success", "Disconnected from server!");
}
@ -94,6 +85,7 @@ public class InteractionUtil {
try {
CodeMP.joinWorkspace(workspaceId);
MemoryManager.startWorkspaceLifetime(workspaceId);
} catch(ConnectionException e) {
InteractionUtil.notifyError(project, String.format(
"Failed to join workspace %s!",
@ -102,70 +94,16 @@ public class InteractionUtil {
return;
}
Disposable lifetime = MemoryManager.getWorkspaceLifetime(workspaceId);
assert lifetime != null; // can never fail
EditorEventMulticaster eventMulticaster = EditorFactory.getInstance().getEventMulticaster();
eventMulticaster.addDocumentListener(new BufferEventListener()); // TODO disposable
eventMulticaster.addCaretListener(new CursorEventListener()); // TODO disposable
eventMulticaster.addDocumentListener(new BufferEventListener(), lifetime);
eventMulticaster.addCaretListener(new CursorEventListener(), lifetime);
CodeMP.getActiveWorkspace().getCursor().callback(controller -> {
new Thread(() -> {
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().runReadAction(() -> {
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.endRow) + event.endCol;
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) {} // don't crash over a bad cursor event
}
} catch(ControllerException ex) {
notifyError(project, "Error receiving change", ex);
}
}).start();
new CursorCallback(this.myProject).accept(controller);
});
if(after != null) after.run();
@ -181,6 +119,7 @@ public class InteractionUtil {
public static void leaveWorkspace(Project project, String workspaceId) {
CodeMP.leaveWorkspace();
MemoryManager.endWorkspaceLifetime(workspaceId);
notifyInfo(
project,
"Success",
@ -201,12 +140,12 @@ public class InteractionUtil {
public static Optional<BufferController> bufferAttach(Project project, Workspace workspace, String path) {
try {
BufferController controller = workspace.attachToBuffer(path);
MemoryManager.startBufferLifetime(workspace.getWorkspaceId(), 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);
@ -214,13 +153,22 @@ public class InteractionUtil {
}
}
private static void notifyInfo(Project project, String title, String msg) {
public static void bufferCreate(Project project, String path) {
try {
Workspace workspace = CodeMP.getActiveWorkspace();
workspace.createBuffer(path);
} catch(ConnectionRemoteException e) {
notifyError(project, "Failed to create a buffer!", e);
}
}
public 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) {
public 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()),

View file

@ -12,7 +12,8 @@ import java.util.concurrent.ConcurrentHashMap;
* Allows association of IntelliJ {@link Disposable Disposables} with CodeMP-related
* lifetimes (which are managed by a {@link Cleaner}).
*/
public class CodeMPMemoryManager {
@SuppressWarnings("UnusedReturnValue")
public class MemoryManager {
private static ClientDisposable clientDisposable = null;
public static boolean startClientLifetime() {
@ -35,7 +36,7 @@ public class CodeMPMemoryManager {
public static boolean startWorkspaceLifetime(String workspace) {
if(clientDisposable.workspaces.containsKey(workspace)) return false;
clientDisposable.workspaces.put(workspace, new DisposableWorkspace());
clientDisposable.workspaces.put(workspace, new WorkspaceDisposable());
return true;
}
@ -43,29 +44,37 @@ public class CodeMPMemoryManager {
return clientDisposable.workspaces.get(workspace);
}
public static boolean endWorkspaceLifetime(String workspace, String buffer) {
if(clientDisposable == null) return false;
ClientDisposable tmp = clientDisposable;
clientDisposable = null;
Disposer.dispose(tmp);
public static boolean endWorkspaceLifetime(String workspace) {
WorkspaceDisposable ws = clientDisposable.workspaces.remove(workspace);
if(ws == null) return false;
Disposer.dispose(ws);
return true;
}
public static boolean startBufferLifetime(String workspace, String buffer) {
WorkspaceDisposable ws = (WorkspaceDisposable) getWorkspaceLifetime(workspace);
if(ws == null || ws.buffers.containsKey(buffer)) return false;
ws.buffers.put(buffer, Disposer.newDisposable());
return true;
}
public static @Nullable Disposable getBufferLifetime(String workspace, String buffer) {
WorkspaceDisposable ws = (WorkspaceDisposable) getWorkspaceLifetime(workspace);
if(ws == null) return null;
return ws.buffers.get(buffer);
}
public static boolean endBufferLifetime(String workspace, String buffer) {
WorkspaceDisposable ws = (WorkspaceDisposable) getWorkspaceLifetime(workspace);
if(ws == null) return false;
Disposable buf = ws.buffers.get(buffer);
if(buf == null) return false;
Disposer.dispose(buf);
return true;
}
private static class ClientDisposable implements Disposable {
private final Map<String, DisposableWorkspace> workspaces = new ConcurrentHashMap<>();
private final Map<String, WorkspaceDisposable> workspaces = new ConcurrentHashMap<>();
@Override
public void dispose() {
this.workspaces.values().forEach(Disposer::dispose);
@ -73,9 +82,8 @@ public class CodeMPMemoryManager {
}
}
private static class DisposableWorkspace implements Disposable {
private static class WorkspaceDisposable implements Disposable {
private final Map<String, Disposable> buffers = new ConcurrentHashMap<>();
@Override
public void dispose() {
this.buffers.values().forEach(Disposer::dispose);

View file

@ -1,12 +1,65 @@
package mp.code.intellij.util;
package mp.code.intellij.util.cb;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import lombok.RequiredArgsConstructor;
import mp.code.BufferController;
import mp.code.data.TextChange;
import mp.code.exceptions.ControllerException;
import mp.code.intellij.CodeMP;
import mp.code.intellij.util.FileUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
@RequiredArgsConstructor
public class BufferCallback implements Consumer<BufferController> {
private static final Executor BUFFER_EXECUTOR = Executors.newSingleThreadExecutor();
private final Project project;
@Override
public void accept(BufferController bufferController) {
BUFFER_EXECUTOR.execute(() -> {
ApplicationManager.getApplication().runReadAction(() -> {
Editor editor = FileUtil.getActiveEditorByPath(this.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(
this.project,
() -> changeList.forEach((change) ->
editor.getDocument().replaceString(
(int) change.start, (int) change.end, change.content)
),
"CodeMPBufferReceive",
"codemp-buffer-receive",
editor.getDocument()
)
);
});
});
});
}
}

View file

@ -1,4 +1,91 @@
package mp.code.intellij.util.cb;
public class CursorCallback {
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Editor;
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.project.Project;
import lombok.RequiredArgsConstructor;
import mp.code.CursorController;
import mp.code.data.Cursor;
import mp.code.exceptions.ControllerException;
import mp.code.intellij.CodeMP;
import mp.code.intellij.util.ColorUtil;
import mp.code.intellij.util.FileUtil;
import mp.code.intellij.util.InteractionUtil;
import java.awt.*;
import java.util.Optional;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
@RequiredArgsConstructor
public class CursorCallback implements Consumer<CursorController> {
private static final Executor CURSOR_EXECUTOR = Executors.newSingleThreadExecutor();
private final Project project;
@Override
public void accept(CursorController controller) {
CURSOR_EXECUTOR.execute(() -> { // necessary
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().runReadAction(() -> {
Editor editor = FileUtil.getActiveEditorByPath(this.project, event.buffer);
if(editor == null) return;
int startOffset = editor.getDocument().getLineStartOffset(event.startRow) + event.startCol;
int endOffset = editor.getDocument().getLineStartOffset(event.endRow) + event.endCol;
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) {} // don't crash over a bad cursor event
}
} catch(ControllerException ex) {
InteractionUtil.notifyError(project, "Error receiving change", ex);
}
});
}
}

View file

@ -35,7 +35,7 @@
<workspaceModel.fileIndexContributor implementation="mp.code.intellij.vfs.CodeMPFileIndexContributor" />
<toolWindow
id="CodeMP"
factoryClass="mp.code.intellij.ui.CodeMPToolWindowFactory"
factoryClass="mp.code.intellij.ui.CodeMPWindowFactory"
anchor="right"
doNotActivateOnStart="false" />
</extensions>