mirror of
https://github.com/hexedtech/codemp-intellij.git
synced 2024-11-21 14:44:49 +01:00
feat(java): initial (broken) VFS implementation
This commit is contained in:
parent
c949f13006
commit
b05b1aaa3e
18 changed files with 622 additions and 105 deletions
|
@ -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.
|
||||
|
||||
|
@ -9,9 +9,11 @@ as well as a remote virtual workspace for you and your team.
|
|||
> [!WARNING]
|
||||
> `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
|
||||
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.
|
||||
|
|
|
@ -33,7 +33,7 @@ intellij {
|
|||
shadowJar {
|
||||
archiveClassifier.set('')
|
||||
dependencies {
|
||||
include(dependency(files('../../lib/java/build/libs/codemp-6645c46.jar')))
|
||||
include(dependency(files('../../lib/java/build/libs/codemp-0.6.2.jar')))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package mp.code.intellij;
|
||||
|
||||
import mp.code.exceptions.ConnectionException;
|
||||
import mp.code.intellij.exceptions.ide.NotConnectedException;
|
||||
import mp.code.intellij.workspace.IJWorkspace;
|
||||
import mp.code.Client;
|
||||
import mp.code.exceptions.CodeMPException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
@ -15,7 +15,7 @@ public class CodeMP {
|
|||
public static final Map<String, IJWorkspace> ACTIVE_WORKSPACES = new ConcurrentHashMap<>();
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,19 +1,18 @@
|
|||
package mp.code.intellij.actions;
|
||||
|
||||
import com.intellij.credentialStore.Credentials;
|
||||
import mp.code.exceptions.ConnectionException;
|
||||
import mp.code.intellij.CodeMP;
|
||||
import mp.code.intellij.settings.CodeMPSettings;
|
||||
import mp.code.intellij.util.ActionUtil;
|
||||
import com.intellij.openapi.actionSystem.AnAction;
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent;
|
||||
import mp.code.exceptions.CodeMPException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
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());
|
||||
Credentials creds = Objects.requireNonNull(state.getCredentials());
|
||||
CodeMP.connect(
|
||||
|
|
|
@ -1,20 +1,36 @@
|
|||
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.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.AnActionEvent;
|
||||
import com.intellij.openapi.ui.Messages;
|
||||
import mp.code.exceptions.CodeMPException;
|
||||
import org.apache.logging.log4j.util.Strings;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class WorkspaceJoinAction extends AnAction {
|
||||
public static void join(AnActionEvent e, String workspaceId, boolean silent) throws CodeMPException {
|
||||
CodeMP.ACTIVE_WORKSPACES.put(workspaceId, new IJWorkspace(
|
||||
workspaceId, CodeMP.getClient("join workspace"),
|
||||
false, e.getProject() //TODO: implement remote projects
|
||||
));
|
||||
public static void join(AnActionEvent e, String workspaceId, boolean silent) throws ConnectionException, IOException {
|
||||
CodeMP.getClient("join workspace").joinWorkspace(workspaceId);
|
||||
CodeMPFileSystem fs = (CodeMPFileSystem) VirtualFileManager.getInstance().getFileSystem(CodeMPFileSystem.PROTOCOL);
|
||||
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,
|
||||
"Success", String.format("Joined workspace %s!", workspaceId));
|
||||
|
|
|
@ -7,10 +7,8 @@ import com.intellij.openapi.editor.event.DocumentEvent;
|
|||
import com.intellij.openapi.editor.event.DocumentListener;
|
||||
import mp.code.BufferController;
|
||||
import mp.code.data.TextChange;
|
||||
import mp.code.exceptions.CodeMPException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
public class BufferEventListener implements DocumentListener {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package mp.code.intellij.listeners;
|
||||
|
||||
import mp.code.exceptions.ConnectionException;
|
||||
import mp.code.intellij.task.BufferEventAwaiterTask;
|
||||
import mp.code.intellij.util.FileUtil;
|
||||
import com.intellij.openapi.Disposable;
|
||||
|
@ -11,7 +12,6 @@ import com.intellij.openapi.util.Disposer;
|
|||
import com.intellij.openapi.vfs.VirtualFile;
|
||||
import mp.code.BufferController;
|
||||
import mp.code.Workspace;
|
||||
import mp.code.exceptions.CodeMPException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
@ -56,10 +56,11 @@ public class WorkspaceFileOpenedListener implements FileOpenedSyncListener {
|
|||
private BufferController getBufferForPath(String path) {
|
||||
try {
|
||||
return this.handler.attachToBuffer(path);
|
||||
} catch (CodeMPException ignored) {
|
||||
} catch (ConnectionException ignored) {
|
||||
try {
|
||||
return this.handler.createBuffer(path);
|
||||
} catch(CodeMPException e) {
|
||||
this.handler.createBuffer(path);
|
||||
return this.handler.attachToBuffer(path);
|
||||
} catch(ConnectionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import com.intellij.openapi.components.PersistentStateComponent;
|
|||
import com.intellij.openapi.components.State;
|
||||
import com.intellij.openapi.components.Storage;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
|
@ -33,16 +34,26 @@ public class CodeMPSettings implements PersistentStateComponent<CodeMPSettings.S
|
|||
this.currentState = state;
|
||||
}
|
||||
|
||||
private static final String KEY = "cred";
|
||||
static CredentialAttributes createCredentialAttributes() {
|
||||
return new CredentialAttributes(CredentialAttributesKt.generateServiceName("CodeMP", KEY));
|
||||
}
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public static class State {
|
||||
@Getter String serverUrl;
|
||||
String serverUrl;
|
||||
|
||||
private static CredentialAttributes createCredentialAttributes() {
|
||||
return new CredentialAttributes(CredentialAttributesKt.generateServiceName(
|
||||
"CodeMP",
|
||||
"login"
|
||||
));
|
||||
}
|
||||
|
||||
public @Nullable Credentials getCredentials() {
|
||||
CredentialAttributes attr = createCredentialAttributes();
|
||||
return PasswordSafe.getInstance().get(attr);
|
||||
}
|
||||
|
||||
public void setCredentials(Credentials creds) {
|
||||
CredentialAttributes attributes = createCredentialAttributes();
|
||||
PasswordSafe.getInstance().set(attributes, creds);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
package mp.code.intellij.settings;
|
||||
|
||||
import com.intellij.credentialStore.CredentialAttributes;
|
||||
import com.intellij.credentialStore.Credentials;
|
||||
import com.intellij.credentialStore.OneTimeString;
|
||||
import com.intellij.ide.passwordSafe.PasswordSafe;
|
||||
import com.intellij.openapi.options.Configurable;
|
||||
import com.intellij.ui.components.JBLabel;
|
||||
import com.intellij.ui.components.JBPasswordField;
|
||||
import com.intellij.ui.components.JBTextField;
|
||||
import com.intellij.util.ui.FormBuilder;
|
||||
import com.intellij.util.ui.JBFont;
|
||||
import org.jetbrains.annotations.Nls;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
|
@ -53,8 +52,7 @@ final class CodeMPSettingsConfigurable implements Configurable {
|
|||
public void apply() {
|
||||
CodeMPSettings.State state = Objects.requireNonNull(CodeMPSettings.getInstance().getState());
|
||||
state.serverUrl = this.component.serverUrlField.getText();
|
||||
CredentialAttributes attributes = CodeMPSettings.createCredentialAttributes();
|
||||
PasswordSafe.getInstance().set(attributes, new Credentials(
|
||||
state.setCredentials(new Credentials(
|
||||
this.component.userNameField.getText(),
|
||||
this.component.passwordField.getPassword()
|
||||
));
|
||||
|
@ -70,7 +68,6 @@ final class CodeMPSettingsConfigurable implements Configurable {
|
|||
this.component.userNameField.setText(cred.getUserName());
|
||||
this.component.passwordField.setText(cred.getPasswordAsString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -86,6 +83,7 @@ final class CodeMPSettingsConfigurable implements Configurable {
|
|||
|
||||
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)
|
||||
|
|
|
@ -13,8 +13,6 @@ import com.intellij.openapi.project.Project;
|
|||
import mp.code.BufferController;
|
||||
import mp.code.Workspace;
|
||||
import mp.code.data.TextChange;
|
||||
import mp.code.exceptions.CodeMPException;
|
||||
import mp.code.exceptions.DeadlockedException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
@ -49,10 +47,7 @@ public class BufferEventAwaiterTask extends Task.Backgroundable implements Dispo
|
|||
Optional<TextChange> changeOptional;
|
||||
try {
|
||||
changeOptional = buffer.tryRecv();
|
||||
} catch(DeadlockedException e) {
|
||||
CodeMP.LOGGER.error(e.getMessage());
|
||||
continue;
|
||||
} catch(CodeMPException e) {
|
||||
} catch(Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package mp.code.intellij.task;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import mp.code.exceptions.DeadlockedException;
|
||||
import mp.code.intellij.CodeMP;
|
||||
import mp.code.intellij.util.ColorUtil;
|
||||
import mp.code.intellij.util.FileUtil;
|
||||
|
@ -18,7 +17,6 @@ import com.intellij.openapi.progress.Task;
|
|||
import com.intellij.openapi.project.Project;
|
||||
import mp.code.CursorController;
|
||||
import mp.code.data.Cursor;
|
||||
import mp.code.exceptions.CodeMPException;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.awt.*;
|
||||
|
|
|
@ -7,8 +7,13 @@ 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 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.Optional;
|
||||
|
||||
public class FileUtil {
|
||||
public static String getRelativePath(Project project, VirtualFile vf) {
|
||||
|
@ -29,4 +34,26 @@ public class FileUtil {
|
|||
.findFirst()
|
||||
.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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
111
src/main/java/mp/code/intellij/vfs/CodeMPFile.java
Normal file
111
src/main/java/mp/code/intellij/vfs/CodeMPFile.java
Normal 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();
|
||||
}
|
||||
}
|
210
src/main/java/mp/code/intellij/vfs/CodeMPFileSystem.java
Normal file
210
src/main/java/mp/code/intellij/vfs/CodeMPFileSystem.java
Normal 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();
|
||||
}
|
||||
}
|
83
src/main/java/mp/code/intellij/vfs/CodeMPFolder.java
Normal file
83
src/main/java/mp/code/intellij/vfs/CodeMPFolder.java
Normal 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");
|
||||
}
|
||||
}
|
127
src/main/java/mp/code/intellij/vfs/CodeMPPath.java
Normal file
127
src/main/java/mp/code/intellij/vfs/CodeMPPath.java
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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() {}
|
||||
}
|
|
@ -19,7 +19,11 @@
|
|||
</group>
|
||||
</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"/>
|
||||
<applicationService serviceImplementation="mp.code.intellij.settings.CodeMPSettings"/>
|
||||
<applicationConfigurable
|
||||
|
|
Loading…
Reference in a new issue