diff --git a/README.md b/README.md index 5b4939c..2cd4cb8 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/build.gradle b/build.gradle index 242e763..8a4a5c9 100644 --- a/build.gradle +++ b/build.gradle @@ -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'))) } } diff --git a/src/main/java/mp/code/intellij/CodeMP.java b/src/main/java/mp/code/intellij/CodeMP.java index d64a532..3676a5a 100644 --- a/src/main/java/mp/code/intellij/CodeMP.java +++ b/src/main/java/mp/code/intellij/CodeMP.java @@ -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 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); } diff --git a/src/main/java/mp/code/intellij/actions/ConnectAction.java b/src/main/java/mp/code/intellij/actions/ConnectAction.java index c435e5b..27ebc47 100644 --- a/src/main/java/mp/code/intellij/actions/ConnectAction.java +++ b/src/main/java/mp/code/intellij/actions/ConnectAction.java @@ -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( diff --git a/src/main/java/mp/code/intellij/actions/workspace/WorkspaceJoinAction.java b/src/main/java/mp/code/intellij/actions/workspace/WorkspaceJoinAction.java index 2f1bfae..0a7ad49 100644 --- a/src/main/java/mp/code/intellij/actions/workspace/WorkspaceJoinAction.java +++ b/src/main/java/mp/code/intellij/actions/workspace/WorkspaceJoinAction.java @@ -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)); diff --git a/src/main/java/mp/code/intellij/listeners/BufferEventListener.java b/src/main/java/mp/code/intellij/listeners/BufferEventListener.java index 26bfa87..528a349 100644 --- a/src/main/java/mp/code/intellij/listeners/BufferEventListener.java +++ b/src/main/java/mp/code/intellij/listeners/BufferEventListener.java @@ -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 { diff --git a/src/main/java/mp/code/intellij/listeners/WorkspaceFileOpenedListener.java b/src/main/java/mp/code/intellij/listeners/WorkspaceFileOpenedListener.java index 4f9df0b..9373110 100644 --- a/src/main/java/mp/code/intellij/listeners/WorkspaceFileOpenedListener.java +++ b/src/main/java/mp/code/intellij/listeners/WorkspaceFileOpenedListener.java @@ -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); } } diff --git a/src/main/java/mp/code/intellij/settings/CodeMPSettings.java b/src/main/java/mp/code/intellij/settings/CodeMPSettings.java index b6c8823..befb508 100644 --- a/src/main/java/mp/code/intellij/settings/CodeMPSettings.java +++ b/src/main/java/mp/code/intellij/settings/CodeMPSettings.java @@ -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 changeOptional; try { changeOptional = buffer.tryRecv(); - } catch(DeadlockedException e) { - CodeMP.LOGGER.error(e.getMessage()); - continue; - } catch(CodeMPException e) { + } catch(Exception e) { throw new RuntimeException(e); } diff --git a/src/main/java/mp/code/intellij/task/CursorEventAwaiterTask.java b/src/main/java/mp/code/intellij/task/CursorEventAwaiterTask.java index ed7dd8f..bb4b211 100644 --- a/src/main/java/mp/code/intellij/task/CursorEventAwaiterTask.java +++ b/src/main/java/mp/code/intellij/task/CursorEventAwaiterTask.java @@ -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.*; diff --git a/src/main/java/mp/code/intellij/util/FileUtil.java b/src/main/java/mp/code/intellij/util/FileUtil.java index 184074c..378a027 100644 --- a/src/main/java/mp/code/intellij/util/FileUtil.java +++ b/src/main/java/mp/code/intellij/util/FileUtil.java @@ -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 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 controller = ws.getBuffer(path.getRealPath()); + if(controller.isPresent()) return controller; + try { + return Optional.of(ws.attachToBuffer(path.getRealPath())); + } catch(ConnectionException e) { + return Optional.empty(); + } + }); + } } diff --git a/src/main/java/mp/code/intellij/vfs/CodeMPFile.java b/src/main/java/mp/code/intellij/vfs/CodeMPFile.java new file mode 100644 index 0000000..ae1f1ca --- /dev/null +++ b/src/main/java/mp/code/intellij/vfs/CodeMPFile.java @@ -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(); + } +} diff --git a/src/main/java/mp/code/intellij/vfs/CodeMPFileSystem.java b/src/main/java/mp/code/intellij/vfs/CodeMPFileSystem.java new file mode 100644 index 0000000..7ccc9b9 --- /dev/null +++ b/src/main/java/mp/code/intellij/vfs/CodeMPFileSystem.java @@ -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 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 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 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 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(); + } +} diff --git a/src/main/java/mp/code/intellij/vfs/CodeMPFolder.java b/src/main/java/mp/code/intellij/vfs/CodeMPFolder.java new file mode 100644 index 0000000..24a79dc --- /dev/null +++ b/src/main/java/mp/code/intellij/vfs/CodeMPFolder.java @@ -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"); + } +} diff --git a/src/main/java/mp/code/intellij/vfs/CodeMPPath.java b/src/main/java/mp/code/intellij/vfs/CodeMPPath.java new file mode 100644 index 0000000..e939535 --- /dev/null +++ b/src/main/java/mp/code/intellij/vfs/CodeMPPath.java @@ -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: codemp://[workspace]/[path]. + * 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 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; + } +} diff --git a/src/main/java/mp/code/intellij/workspace/IJWorkspace.java b/src/main/java/mp/code/intellij/workspace/IJWorkspace.java deleted file mode 100644 index cb7f553..0000000 --- a/src/main/java/mp/code/intellij/workspace/IJWorkspace.java +++ /dev/null @@ -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() {} -} diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 247578a..0a6af7e 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -19,7 +19,11 @@ - + +