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.
@ -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.

View file

@ -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')))
}
}

View file

@ -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);
}

View file

@ -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(

View file

@ -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));

View file

@ -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 {

View file

@ -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);
}
}

View file

@ -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);
}
}
}

View file

@ -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)

View file

@ -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);
}

View file

@ -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.*;

View file

@ -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();
}
});
}
}

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

@ -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