feat(java): initial (broken) VFS implementation

This commit is contained in:
zaaarf 2024-09-09 16:06:54 +02:00
parent c949f13006
commit b05b1aaa3e
No known key found for this signature in database
GPG key ID: C91CFF9E2262BBA1
18 changed files with 622 additions and 105 deletions

View file

@ -1,4 +1,4 @@
[![codemp](https://codemp.dev/static/banner.png)](https://codemp.dev) [![codemp](https://code.mp/static/banner.png)](https://code.mp)
> `codemp` is a **collaborative** text editing solution to work remotely. > `codemp` is a **collaborative** text editing solution to work remotely.
@ -9,9 +9,11 @@ as well as a remote virtual workspace for you and your team.
> [!WARNING] > [!WARNING]
> `codmep-intellij` is still under active development. It is not in a usable state yet, coming soon! > `codmep-intellij` is still under active development. It is not in a usable state yet, coming soon!
This is the reference `codemp` [IntelliJ Platform](https://www.jetbrains.com/opensource/idea/) plugin, maintained by [hexedtech](https://hexed.technology). This is the reference `codemp` [IntelliJ Platform](https://www.jetbrains.com/opensource/idea/) plugin,
maintained by [hexedtech](https://hexed.technology).
## Testing ## Testing
As this is not meant to be used yet, we do not provide build instructions. As this is not meant to be used yet, we do not provide build instructions.
You may however test it using Gradle with the `runIde` task. It will open in a new window an IntelliJ IDE with the plugin installed. You may however test it using Gradle with the `runIde` task. It will open in a new window an IntelliJ IDE
with the plugin installed.

View file

@ -33,7 +33,7 @@ intellij {
shadowJar { shadowJar {
archiveClassifier.set('') archiveClassifier.set('')
dependencies { dependencies {
include(dependency(files('../../lib/java/build/libs/codemp-6645c46.jar'))) include(dependency(files('../../lib/java/build/libs/codemp-0.6.2.jar')))
} }
} }

View file

@ -1,9 +1,9 @@
package mp.code.intellij; package mp.code.intellij;
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.intellij.workspace.IJWorkspace;
import mp.code.Client; import mp.code.Client;
import mp.code.exceptions.CodeMPException;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@ -15,7 +15,7 @@ public class CodeMP {
public static final Map<String, IJWorkspace> ACTIVE_WORKSPACES = new ConcurrentHashMap<>(); public static final Map<String, IJWorkspace> ACTIVE_WORKSPACES = new ConcurrentHashMap<>();
private static Client CLIENT = null; private static Client CLIENT = null;
public static void connect(String url, String username, String password) throws CodeMPException { public static void connect(String url, String username, String password) throws ConnectionException {
CLIENT = Client.connect(url, username, password); CLIENT = Client.connect(url, username, password);
} }

View file

@ -1,19 +1,18 @@
package mp.code.intellij.actions; package mp.code.intellij.actions;
import com.intellij.credentialStore.Credentials; import com.intellij.credentialStore.Credentials;
import mp.code.exceptions.ConnectionException;
import mp.code.intellij.CodeMP; import mp.code.intellij.CodeMP;
import mp.code.intellij.settings.CodeMPSettings; import mp.code.intellij.settings.CodeMPSettings;
import mp.code.intellij.util.ActionUtil; 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.exceptions.CodeMPException;
import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Objects; import java.util.Objects;
public class ConnectAction extends AnAction { public class ConnectAction extends AnAction {
public static void connect(AnActionEvent e, boolean silent) throws NullPointerException, CodeMPException { public static void connect(AnActionEvent e, boolean silent) throws NullPointerException, ConnectionException {
CodeMPSettings.State state = Objects.requireNonNull(CodeMPSettings.getInstance().getState()); CodeMPSettings.State state = Objects.requireNonNull(CodeMPSettings.getInstance().getState());
Credentials creds = Objects.requireNonNull(state.getCredentials()); Credentials creds = Objects.requireNonNull(state.getCredentials());
CodeMP.connect( CodeMP.connect(

View file

@ -1,20 +1,36 @@
package mp.code.intellij.actions.workspace; package mp.code.intellij.actions.workspace;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ModuleRootModificationUtil;
import com.intellij.openapi.vfs.VirtualFileManager;
import mp.code.exceptions.ConnectionException;
import mp.code.intellij.CodeMP; import mp.code.intellij.CodeMP;
import mp.code.intellij.util.ActionUtil; import mp.code.intellij.util.ActionUtil;
import mp.code.intellij.workspace.IJWorkspace; 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 mp.code.exceptions.CodeMPException; 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 CodeMPException { public static void join(AnActionEvent e, String workspaceId, boolean silent) throws ConnectionException, IOException {
CodeMP.ACTIVE_WORKSPACES.put(workspaceId, new IJWorkspace( CodeMP.getClient("join workspace").joinWorkspace(workspaceId);
workspaceId, CodeMP.getClient("join workspace"), CodeMPFileSystem fs = (CodeMPFileSystem) VirtualFileManager.getInstance().getFileSystem(CodeMPFileSystem.PROTOCOL);
false, e.getProject() //TODO: implement remote projects CodeMPFolder root = new CodeMPFolder(fs, new CodeMPPath(workspaceId, Strings.EMPTY));
));
Project proj = e.getProject();
assert proj != null;
Module someModule = ModuleManager.getInstance(proj).getModules()[0];
ModuleRootModificationUtil.addContentRoot(someModule, root);
if(!silent) ActionUtil.notify(e, if(!silent) ActionUtil.notify(e,
"Success", String.format("Joined workspace %s!", workspaceId)); "Success", String.format("Joined workspace %s!", workspaceId));

View file

@ -7,10 +7,8 @@ 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.BufferController;
import mp.code.data.TextChange; import mp.code.data.TextChange;
import mp.code.exceptions.CodeMPException;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.Optional;
import java.util.OptionalLong; import java.util.OptionalLong;
public class BufferEventListener implements DocumentListener { public class BufferEventListener implements DocumentListener {

View file

@ -1,5 +1,6 @@
package mp.code.intellij.listeners; package mp.code.intellij.listeners;
import mp.code.exceptions.ConnectionException;
import mp.code.intellij.task.BufferEventAwaiterTask; import mp.code.intellij.task.BufferEventAwaiterTask;
import mp.code.intellij.util.FileUtil; import mp.code.intellij.util.FileUtil;
import com.intellij.openapi.Disposable; import com.intellij.openapi.Disposable;
@ -11,7 +12,6 @@ import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFile;
import mp.code.BufferController; import mp.code.BufferController;
import mp.code.Workspace; import mp.code.Workspace;
import mp.code.exceptions.CodeMPException;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.List; import java.util.List;
@ -56,10 +56,11 @@ public class WorkspaceFileOpenedListener implements FileOpenedSyncListener {
private BufferController getBufferForPath(String path) { private BufferController getBufferForPath(String path) {
try { try {
return this.handler.attachToBuffer(path); return this.handler.attachToBuffer(path);
} catch (CodeMPException ignored) { } catch (ConnectionException ignored) {
try { try {
return this.handler.createBuffer(path); this.handler.createBuffer(path);
} catch(CodeMPException e) { return this.handler.attachToBuffer(path);
} catch(ConnectionException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }
} }

View file

@ -9,6 +9,7 @@ import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State; import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage; import com.intellij.openapi.components.Storage;
import lombok.Getter; import lombok.Getter;
import lombok.Setter;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -33,16 +34,26 @@ public class CodeMPSettings implements PersistentStateComponent<CodeMPSettings.S
this.currentState = state; this.currentState = state;
} }
private static final String KEY = "cred"; @Getter
static CredentialAttributes createCredentialAttributes() { @Setter
return new CredentialAttributes(CredentialAttributesKt.generateServiceName("CodeMP", KEY)); public static class State {
String serverUrl;
private static CredentialAttributes createCredentialAttributes() {
return new CredentialAttributes(CredentialAttributesKt.generateServiceName(
"CodeMP",
"login"
));
} }
public static class State {
@Getter String serverUrl;
public @Nullable Credentials getCredentials() { public @Nullable Credentials getCredentials() {
CredentialAttributes attr = createCredentialAttributes(); CredentialAttributes attr = createCredentialAttributes();
return PasswordSafe.getInstance().get(attr); return PasswordSafe.getInstance().get(attr);
} }
public void setCredentials(Credentials creds) {
CredentialAttributes attributes = createCredentialAttributes();
PasswordSafe.getInstance().set(attributes, creds);
}
} }
} }

View file

@ -1,14 +1,13 @@
package mp.code.intellij.settings; package mp.code.intellij.settings;
import com.intellij.credentialStore.CredentialAttributes;
import com.intellij.credentialStore.Credentials; import com.intellij.credentialStore.Credentials;
import com.intellij.credentialStore.OneTimeString; import com.intellij.credentialStore.OneTimeString;
import com.intellij.ide.passwordSafe.PasswordSafe;
import com.intellij.openapi.options.Configurable; import com.intellij.openapi.options.Configurable;
import com.intellij.ui.components.JBLabel; import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBPasswordField; import com.intellij.ui.components.JBPasswordField;
import com.intellij.ui.components.JBTextField; import com.intellij.ui.components.JBTextField;
import com.intellij.util.ui.FormBuilder; import com.intellij.util.ui.FormBuilder;
import com.intellij.util.ui.JBFont;
import org.jetbrains.annotations.Nls; import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -53,8 +52,7 @@ final class CodeMPSettingsConfigurable implements Configurable {
public void apply() { public void apply() {
CodeMPSettings.State state = Objects.requireNonNull(CodeMPSettings.getInstance().getState()); CodeMPSettings.State state = Objects.requireNonNull(CodeMPSettings.getInstance().getState());
state.serverUrl = this.component.serverUrlField.getText(); state.serverUrl = this.component.serverUrlField.getText();
CredentialAttributes attributes = CodeMPSettings.createCredentialAttributes(); state.setCredentials(new Credentials(
PasswordSafe.getInstance().set(attributes, new Credentials(
this.component.userNameField.getText(), this.component.userNameField.getText(),
this.component.passwordField.getPassword() this.component.passwordField.getPassword()
)); ));
@ -70,7 +68,6 @@ final class CodeMPSettingsConfigurable implements Configurable {
this.component.userNameField.setText(cred.getUserName()); this.component.userNameField.setText(cred.getUserName());
this.component.passwordField.setText(cred.getPasswordAsString()); this.component.passwordField.setText(cred.getPasswordAsString());
} }
} }
@Override @Override
@ -86,6 +83,7 @@ final class CodeMPSettingsConfigurable implements Configurable {
Component() { Component() {
this.mainPanel = FormBuilder.createFormBuilder() this.mainPanel = FormBuilder.createFormBuilder()
.addComponent(new JBLabel("Connection").withFont(JBFont.h2().asBold()))
.addLabeledComponent(new JBLabel("Server address:"), this.serverUrlField, 1, false) .addLabeledComponent(new JBLabel("Server address:"), this.serverUrlField, 1, false)
.addLabeledComponent(new JBLabel("Username:"), this.userNameField, 1, false) .addLabeledComponent(new JBLabel("Username:"), this.userNameField, 1, false)
.addLabeledComponent(new JBLabel("Password:"), this.passwordField, 1, false) .addLabeledComponent(new JBLabel("Password:"), this.passwordField, 1, false)

View file

@ -13,8 +13,6 @@ import com.intellij.openapi.project.Project;
import mp.code.BufferController; import mp.code.BufferController;
import mp.code.Workspace; import mp.code.Workspace;
import mp.code.data.TextChange; import mp.code.data.TextChange;
import mp.code.exceptions.CodeMPException;
import mp.code.exceptions.DeadlockedException;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
@ -49,10 +47,7 @@ public class BufferEventAwaiterTask extends Task.Backgroundable implements Dispo
Optional<TextChange> changeOptional; Optional<TextChange> changeOptional;
try { try {
changeOptional = buffer.tryRecv(); changeOptional = buffer.tryRecv();
} catch(DeadlockedException e) { } catch(Exception e) {
CodeMP.LOGGER.error(e.getMessage());
continue;
} catch(CodeMPException e) {
throw new RuntimeException(e); throw new RuntimeException(e);
} }

View file

@ -1,7 +1,6 @@
package mp.code.intellij.task; package mp.code.intellij.task;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import mp.code.exceptions.DeadlockedException;
import mp.code.intellij.CodeMP; import mp.code.intellij.CodeMP;
import mp.code.intellij.util.ColorUtil; import mp.code.intellij.util.ColorUtil;
import mp.code.intellij.util.FileUtil; import mp.code.intellij.util.FileUtil;
@ -18,7 +17,6 @@ import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project; import com.intellij.openapi.project.Project;
import mp.code.CursorController; import mp.code.CursorController;
import mp.code.data.Cursor; import mp.code.data.Cursor;
import mp.code.exceptions.CodeMPException;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.awt.*; import java.awt.*;

View file

@ -7,8 +7,13 @@ import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectRootManager; import com.intellij.openapi.roots.ProjectRootManager;
import com.intellij.openapi.vfs.VfsUtilCore; 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.exceptions.ConnectionException;
import mp.code.intellij.CodeMP;
import mp.code.intellij.vfs.CodeMPPath;
import java.util.Arrays; import java.util.Arrays;
import java.util.Optional;
public class FileUtil { public class FileUtil {
public static String getRelativePath(Project project, VirtualFile vf) { public static String getRelativePath(Project project, VirtualFile vf) {
@ -29,4 +34,26 @@ public class FileUtil {
.findFirst() .findFirst()
.orElse(null); .orElse(null);
} }
/**
* Will first check if such a buffer exists.
* If it does, it will try to get the relevant controller and,
* if necessary, will attach to the buffer.
* @return the relevant {@link BufferController}, if it could be obtained
*/
public static Optional<BufferController> getRelevantBufferController(CodeMPPath path) {
return CodeMP.getClient("buffer access")
.getWorkspace(path.getWorkspaceName())
.flatMap(ws -> {
String[] matches = ws.getFileTree(Optional.of(path.getRealPath()));
if(matches.length == 0) return Optional.empty();
Optional<BufferController> controller = ws.getBuffer(path.getRealPath());
if(controller.isPresent()) return controller;
try {
return Optional.of(ws.attachToBuffer(path.getRealPath()));
} catch(ConnectionException e) {
return Optional.empty();
}
});
}
} }

View file

@ -0,0 +1,111 @@
package mp.code.intellij.vfs;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.vfs.VirtualFile;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import mp.code.exceptions.ControllerException;
import mp.code.intellij.CodeMP;
import mp.code.intellij.util.FileUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Path;
import java.util.Optional;
@Getter
@RequiredArgsConstructor
public class CodeMPFile extends VirtualFile {
protected final CodeMPFileSystem fileSystem;
protected final CodeMPPath path;
@Override
public @NotNull @NlsSafe String getName() {
return this.path.getFileName();
}
@Override
public @NonNls @NotNull String getPath() {
return this.path.join();
}
@Override
public boolean isWritable() {
return true; // TODO permissions!
}
@Override
public boolean isDirectory() {
return false; // TODO ????
}
@Override
public boolean isValid() {
return CodeMP.getClient("validity check")
.getWorkspace(this.path.getWorkspaceName())
.map(ws -> ws.getFileTree(Optional.of(this.path.getRealPath())))
.map(buf -> buf.length != 0)
.orElse(false);
}
@Override
public @Nullable CodeMPFolder getParent() {
return this.path.getParent()
.map(parent -> new CodeMPFolder(this.fileSystem, parent))
.orElse(null);
}
@Override
public CodeMPFile[] getChildren() {
return null;
}
@Override
public @NotNull OutputStream getOutputStream(Object requester, long newModificationStamp, long newTimeStamp) throws IOException {
throw new RuntimeException("WHAT OUTPUT");
}
@Override
public byte @NotNull [] contentsToByteArray() throws IOException {
return FileUtil.getRelevantBufferController(this.path).flatMap(c -> {
try {
return Optional.of(c.getContent().getBytes());
} catch(ControllerException e) {
return Optional.empty();
}
}).orElseThrow(() -> new IOException("Buffer " + this.path.join() + "did not exist or was inaccessible!"));
}
@Override
public long getTimeStamp() {
return System.currentTimeMillis();
}
@Override
public long getLength() {
try {
return this.contentsToByteArray().length;
} catch(IOException e) {
throw new RuntimeException(e);
}
}
@Override
public void refresh(boolean asynchronous, boolean recursive, @Nullable Runnable postRunnable) {
// TODO
}
@Override
public @NotNull InputStream getInputStream() throws IOException {
throw new RuntimeException("WHAT INPUT");
}
@Override
public @NotNull Path toNioPath() {
return this.path.toNioPath();
}
}

View file

@ -0,0 +1,210 @@
package mp.code.intellij.vfs;
import com.google.common.collect.Sets;
import com.intellij.credentialStore.Credentials;
import com.intellij.openapi.vfs.*;
import lombok.Getter;
import mp.code.BufferController;
import mp.code.Workspace;
import mp.code.data.TextChange;
import mp.code.exceptions.ConnectionException;
import mp.code.exceptions.ConnectionRemoteException;
import mp.code.exceptions.ControllerException;
import mp.code.intellij.CodeMP;
import mp.code.intellij.exceptions.ide.NotConnectedException;
import mp.code.intellij.settings.CodeMPSettings;
import mp.code.intellij.util.FileUtil;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalLong;
import java.util.Set;
import java.util.function.BiConsumer;
/**
* VFS implementation representing a remote CodeMP workspace.
* TODO: KNOWN PROBLEMS
* - Pretty sure we need a folder registry somewhere. Doubt he tracks them itself.
* - Already open remote module will crash if not for that janky try-catch in {@link #findFileByPath(String)}, maybe
* try to connect quietly?
*/
@Getter
public class CodeMPFileSystem extends VirtualFileSystem {
public static String PROTOCOL = "codemp";
private final Set<VirtualFileListener> listeners;
public CodeMPFileSystem() {
this.listeners = Sets.newConcurrentHashSet();
}
@Override
public @NonNls @NotNull String getProtocol() {
return PROTOCOL; //TODO: should be same as KeyedLazyInstance.key wtf is that
}
@Override
public @Nullable CodeMPFile findFileByPath(@NotNull @NonNls String path) {
CodeMPPath cmpPath = new CodeMPPath(path);
try {
return CodeMP.getClient("file seek")
.getWorkspace(cmpPath.getWorkspaceName())
.filter(ws -> ws.getFileTree(Optional.of(cmpPath.getRealPath())).length != 0)
.map(ws -> new CodeMPFile(this, cmpPath))
.orElseGet(() -> new CodeMPFolder(this, cmpPath));
} catch(NotConnectedException ex) {
CodeMPSettings.State state = Objects.requireNonNull(CodeMPSettings.getInstance().getState());
Credentials credentials = Objects.requireNonNull(state.getCredentials());
try {
CodeMP.connect(
Objects.requireNonNull(state.getServerUrl()),
Objects.requireNonNull(credentials.getUserName()),
Objects.requireNonNull(credentials.getPasswordAsString())
);
return CodeMP.getClient("file seek")
.getWorkspace(cmpPath.getWorkspaceName())
.filter(ws -> ws.getFileTree(Optional.of(cmpPath.getRealPath())).length != 0)
.map(ws -> new CodeMPFile(this, cmpPath))
.orElseGet(() -> new CodeMPFolder(this, cmpPath));
} catch(ConnectionException e) {
return null;
} // TODO this sucks
}
}
@Override
public void refresh(boolean asynchronous) {
// TODO find out if and where ij stores filetree
// this is a no-op
}
@Override
public @Nullable CodeMPFile refreshAndFindFileByPath(@NotNull String path) {
this.refresh(false);
return this.findFileByPath(path);
}
@Override
public void addVirtualFileListener(@NotNull VirtualFileListener listener) {
this.listeners.add(listener);
}
@Override
public void removeVirtualFileListener(@NotNull VirtualFileListener listener) {
this.listeners.remove(listener);
}
@Override
protected void deleteFile(Object requester, @NotNull VirtualFile vFile) throws IOException {
if(vFile instanceof CodeMPFile cmpFile) {
try {
Optional<Workspace> ws = CodeMP.getClient("delete file")
.getWorkspace(cmpFile.path.getWorkspaceName());
if(ws.isPresent()) {
ws.get().deleteBuffer(vFile.getPath());
} else {
throw new IOException("failed to find workspace!"); // TODO do it better
}
} catch(ConnectionRemoteException e) {
throw new IOException(e);
}
}
}
@Override
protected void moveFile(Object requester, @NotNull VirtualFile vFile, @NotNull VirtualFile newParent) throws IOException {
throw new RuntimeException("RENAME NOT SUPPORTED YET!"); // TODO
}
@Override
protected void renameFile(Object requester, @NotNull VirtualFile vFile, @NotNull String newName) throws IOException {
throw new RuntimeException("RENAME NOT SUPPORTED YET!"); // TODO
}
@Override
protected @NotNull CodeMPFile createChildFile(Object requester, @NotNull VirtualFile vDir, @NotNull String fileName) throws IOException {
if(vDir instanceof CodeMPFolder parent) {
try {
Optional<Workspace> ws = CodeMP.getClient("delete file").getWorkspace(parent.path.getWorkspaceName());
if(ws.isPresent()) {
CodeMPPath newFilePath = parent.path.resolve(fileName);
ws.get().createBuffer(newFilePath.getRealPath());
ws.get().attachToBuffer(newFilePath.getRealPath());
return new CodeMPFile(this, newFilePath);
} else {
throw new IOException("failed to find workspace!"); // TODO do it better
}
} catch(ConnectionException e) {
throw new IOException(e);
}
} else {
throw new IOException("Can only create children in CodeMP folders!");
}
}
@Override
protected @NotNull CodeMPFolder createChildDirectory(
Object requester,
@NotNull VirtualFile vDir,
@NotNull String dirName
) throws IOException {
if(vDir instanceof CodeMPFolder parent) {
return new CodeMPFolder(
this,
parent.path.resolve(dirName)
);
} else {
throw new IOException("Can only create children in CodeMP folders!");
}
}
@Override
protected @NotNull CodeMPFile copyFile(Object requester, @NotNull VirtualFile virtualFile, @NotNull VirtualFile newParent, @NotNull String copyName) throws IOException {
if(virtualFile instanceof CodeMPFile cfile) {
try {
CodeMPFile newFile = this.createChildFile(requester, newParent, copyName);
BufferController oldController = FileUtil.getRelevantBufferController(cfile.path)
.orElseThrow(() -> new IOException("Non existing buffer for old file!"));
BufferController destinationController = FileUtil.getRelevantBufferController(cfile.path)
.orElseThrow(() -> new IOException("Non existing buffer for new file!"));
destinationController.send(new TextChange(0, 0, oldController.getContent(), OptionalLong.empty()));
return newFile;
} catch(ControllerException ex) {
throw new IOException(ex);
}
}
throw new IOException("Bad VirtualFile type!");
}
@Override
public boolean isReadOnly() {
return false; // TODO doesnt exist yet
}
private void dispatchEvent(
BiConsumer<VirtualFileListener, VirtualFileEvent> fun,
Object requester,
VirtualFile file,
VirtualFile parent,
long oldModificationStamp,
long newModificationStamp
) {
this.listeners.forEach(listener -> fun.accept(listener, new VirtualFileEvent(
requester,
file,
parent,
oldModificationStamp,
newModificationStamp
)));
}
@Override
public @Nullable Path getNioPath(@NotNull VirtualFile file) {
return file.toNioPath();
}
}

View file

@ -0,0 +1,83 @@
package mp.code.intellij.vfs;
import com.intellij.openapi.vfs.VirtualFile;
import lombok.Getter;
import mp.code.intellij.CodeMP;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Optional;
@Getter
public class CodeMPFolder extends CodeMPFile {
public CodeMPFolder(CodeMPFileSystem fileSystem, CodeMPPath path) {
super(fileSystem, path);
}
@Override
public boolean isWritable() {
return false;
}
@Override
public boolean isDirectory() {
return true;
}
@Override
public boolean isValid() {
return true;
}
@Override
public CodeMPFile[] getChildren() {
return CodeMP.getClient("get folder children")
.getWorkspace(this.path.getWorkspaceName())
.map(ws ->
Arrays.stream(ws.getFileTree(Optional.of(this.path.getRealPath())))
.map(p -> new CodeMPPath(this.path.getWorkspaceName(), p))
.map(CodeMPPath::join)
.map(this.fileSystem::findFileByPath)
.toArray(CodeMPFile[]::new)
).orElseGet(() -> new CodeMPFile[0]);
}
@Override
public @NotNull OutputStream getOutputStream(Object o, long l, long l1) throws IOException {
throw new RuntimeException("WHAT FOLDER OUTPUT");
}
@Override
public byte @NotNull [] contentsToByteArray() throws IOException {
return new byte[0];
}
@Override
public long getTimeStamp() {
return 0;
}
@Override
public long getLength() {
return 0;
}
@Override
public void refresh(boolean asynchronous, boolean recursive, @Nullable Runnable postRunnable) {
for(CodeMPFile vf : this.getChildren()) {
if(recursive || !this.isDirectory()) {
vf.refresh(asynchronous, recursive, postRunnable);
}
}
}
@Override
public @NotNull InputStream getInputStream() throws IOException {
throw new RuntimeException("WHAT FOLDER INPUT");
}
}

View file

@ -0,0 +1,127 @@
package mp.code.intellij.vfs;
import lombok.Getter;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.Optional;
/**
* A utility class representing a path as implemented in CodeMP.
* To represent them in an IntelliJ-compatible way, we use the workspace name
* as "root folder" of all workspace contents.
* Thus, a CodeMP URI looks like this: <code>codemp://[workspace]/[path]</code>.
* This helper class manages just that.
*/
@Getter @Setter
public class CodeMPPath {
/**
* The name of the workspace that contains this path.
*/
private final String workspaceName;
/**
* The real path. May never be null, but may be empty.
* It is guaranteed to not have any trailing slashes.
*/
private final String realPath;
/**
* Builds a new {@link CodeMPPath} from its separate components.
* @param workspaceName the name of the workspace
* @param realPath the name of the underlying path
*/
public CodeMPPath(String workspaceName, String realPath) {
this.workspaceName = workspaceName;
if(!realPath.isEmpty()) realPath = stripTrailingSlashes(realPath);
this.realPath = realPath;
}
/**
* Builds a new {@link CodeMPPath} from a unified path containing both the real path and
* the workspace name.
* @param pathWithWorkspace the unified path
*/
public CodeMPPath(String pathWithWorkspace) {
this.workspaceName = extractWorkspace(pathWithWorkspace);
this.realPath = extractRealPath(pathWithWorkspace);
}
/**
* Joins back into a single string workspace name and path.
* @return the resulting string
*/
public String join() {
return this.workspaceName + '/' + this.realPath;
}
/**
* Recovers just the name of the current file.
* @return a string containing the name
*/
public String getFileName() {
int lastSlashPos = this.realPath.lastIndexOf('/');
if(lastSlashPos == -1) return this.realPath;
else return this.realPath.substring(lastSlashPos + 1, this.realPath.length() - 1);
}
/**
* Gets the parent, if it is present.
* @return the parent
*/
public Optional<CodeMPPath> getParent() {
int lastSlash = this.realPath.lastIndexOf('/');
if(this.realPath.isEmpty()) return Optional.empty();
else if(lastSlash == -1) return Optional.of(new CodeMPPath(this.workspaceName, ""));
else return Optional.of(new CodeMPPath(
this.workspaceName,
this.realPath.substring(0, lastSlash)
));
}
/**
* Resolves one or multiple children against this, assuming that this
* path represents a folder.
* @param firstChild the first, mandatory child to resolve against
* @param children other, eventual children
* @return the build {@link CodeMPPath}
*/
public CodeMPPath resolve(String firstChild, String... children) {
StringBuilder pathBuilder = new StringBuilder(this.realPath)
.append('/')
.append(stripTrailingSlashes(firstChild));
if(children != null)
for(String c : children)
pathBuilder.append('/').append(stripTrailingSlashes(c));
return new CodeMPPath(
this.workspaceName,
pathBuilder.toString()
);
}
/**
* Converts this to a {@link Path}, accounting for system differences in separator.
* @return the built {@link Path}
*/
public @NotNull Path toNioPath() {
String currentSystemSeparator = FileSystems.getDefault().getSeparator();
return Path.of(this.realPath.replace("/", currentSystemSeparator));
}
private static String extractWorkspace(@NotNull String path) {
int firstSlashPosition = path.indexOf('/');
if(firstSlashPosition == -1) return path;
return path.substring(0, path.indexOf('/'));
}
private static String extractRealPath(@NotNull String path) {
return path.substring(path.indexOf('/') + 1);
}
private static String stripTrailingSlashes(@NotNull String s) {
while(s.charAt(s.length() - 1) == '/') s = s.substring(0, s.length() - 1);
return s;
}
}

View file

@ -1,63 +0,0 @@
package mp.code.intellij.workspace;
import mp.code.intellij.listeners.WorkspaceFileOpenedListener;
import mp.code.intellij.task.BufferEventAwaiterTask;
import mp.code.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;
import mp.code.Client;
import mp.code.Workspace;
import mp.code.exceptions.CodeMPException;
import mp.code.intellij.listeners.CursorEventListener;
import mp.code.intellij.listeners.WorkspaceFileClosedListener;
public class IJWorkspace implements Disposable {
public final String id;
public final String url;
public final boolean isRemote;
public final Workspace 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 Client} to use
* @param isRemote whether the project is remote
* @param project the {@link Project} to use
*/
public IJWorkspace(String id, Client client, boolean isRemote, Project project) throws CodeMPException {
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

@ -20,6 +20,10 @@
</actions> </actions>
<extensions defaultExtensionNs="com.intellij"> <extensions defaultExtensionNs="com.intellij">
<virtualFileSystem
id="codemp_vfs"
key="codemp"
implementationClass="mp.code.intellij.vfs.CodeMPFileSystem"/>
<notificationGroup id="CodeMP" displayType="BALLOON"/> <notificationGroup id="CodeMP" displayType="BALLOON"/>
<applicationService serviceImplementation="mp.code.intellij.settings.CodeMPSettings"/> <applicationService serviceImplementation="mp.code.intellij.settings.CodeMPSettings"/>
<applicationConfigurable <applicationConfigurable