From e092ba054862707b12966aacf66f94666e3a6b55 Mon Sep 17 00:00:00 2001 From: MrLetsplay Date: Mon, 18 Dec 2023 22:35:47 +0100 Subject: [PATCH] Add basic editor sync --- .../mrletsplay/shareclient/ShareClient.java | 237 ++++++++++-------- .../shareclient/util/ProjectRelativePath.java | 7 + .../shareclient/util/ShareSession.java | 71 ++++++ .../ShareClientDocumentListener.java | 61 +++++ .../listeners/ShareClientPartListener.java | 30 +-- 5 files changed, 282 insertions(+), 124 deletions(-) create mode 100644 src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientDocumentListener.java diff --git a/src/main/java/me/mrletsplay/shareclient/ShareClient.java b/src/main/java/me/mrletsplay/shareclient/ShareClient.java index 05f3a1c..cfb2d39 100644 --- a/src/main/java/me/mrletsplay/shareclient/ShareClient.java +++ b/src/main/java/me/mrletsplay/shareclient/ShareClient.java @@ -30,6 +30,7 @@ import me.mrletsplay.shareclient.util.ChecksumUtil; import me.mrletsplay.shareclient.util.Peer; import me.mrletsplay.shareclient.util.ProjectRelativePath; import me.mrletsplay.shareclient.util.ShareSession; +import me.mrletsplay.shareclient.util.listeners.ShareClientDocumentListener; import me.mrletsplay.shareclient.util.listeners.ShareClientPageListener; import me.mrletsplay.shareclient.util.listeners.ShareClientPartListener; import me.mrletsplay.shareclient.util.listeners.ShareClientWindowListener; @@ -60,25 +61,33 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS // The shared instance private static ShareClient plugin; - private ShareClientPartListener partListener = new ShareClientPartListener(); + private ShareClientPartListener partListener; private ShareView view; private ShareSession activeSession; - private Map sharedDocuments; - public ShareClient() { - this.sharedDocuments = new HashMap<>(); + } @Override public void start(BundleContext context) throws Exception { super.start(context); plugin = this; + partListener = new ShareClientPartListener(); getPreferenceStore().setDefault(ShareClientPreferences.SERVER_URI, "ws://localhost:5473"); getPreferenceStore().setDefault(ShareClientPreferences.SHOW_CURSORS, true); // new ShareWSClient(URI.create("ws://localhost:5473")).connect(); + + PlatformUI.getWorkbench().addWindowListener(ShareClientWindowListener.INSTANCE); + Arrays.stream(PlatformUI.getWorkbench().getWorkbenchWindows()).forEach(w -> { + w.addPageListener(ShareClientPageListener.INSTANCE); + Arrays.stream(w.getPages()).forEach(p -> { + p.addPartListener(partListener); + Arrays.stream(p.getEditorReferences()).forEach(e -> partListener.addDocumentListener(e)); + }); + }); } @Override @@ -158,113 +167,133 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS @Override public void onMessage(Message message) { - System.out.println("Got: " + message); - if (message instanceof PeerJoinMessage join) { - activeSession.getPeers().add(new Peer(join.peerName(), join.peerSiteID())); - updateView(); - } - - if(message instanceof PeerLeaveMessage leave) { - activeSession.getPeers().removeIf(p -> p.siteID() == leave.peerSiteID()); - updateView(); - } - - if (message instanceof FullSyncMessage sync) { - // TODO: handle FULL_SYNC - ProjectRelativePath path; - try { - path = ProjectRelativePath.of(sync.documentPath()); - } catch (IllegalArgumentException e) { - e.printStackTrace(); - return; + Display.getDefault().asyncExec(() -> { + System.out.println("Got: " + message); + if (message instanceof PeerJoinMessage join) { + activeSession.getPeers().add(new Peer(join.peerName(), join.peerSiteID())); + updateView(); } - IWorkspace workspace = ResourcesPlugin.getWorkspace(); - IWorkspaceRoot workspaceRoot = workspace.getRoot(); - IProject project = workspaceRoot.getProject(path.projectName()); - if(project == null) return; - // TODO: make sure to not overwrite existing non-shared projects - if (!project.exists()) { - IProjectDescription description = workspace.newProjectDescription(path.projectName()); - try { - project.create(description, null); - project.open(null); - } catch (CoreException e) { - e.printStackTrace(); - MessageDialog.openError(null, "Share Client", "Failed to create project: " + e.toString()); - return; - } - System.out.println("Created project " + project); + if(message instanceof PeerLeaveMessage leave) { + activeSession.getPeers().removeIf(p -> p.siteID() == leave.peerSiteID()); + updateView(); } - Path filePath = project.getLocation().toPath().resolve(path.relativePath()); - try { - if (!Files.exists(filePath)) { - Files.createDirectories(filePath.getParent()); - Files.createFile(filePath); - } - - // TODO: update sharedDocuments - - Files.write(filePath, sync.content()); - project.refreshLocal(IResource.DEPTH_INFINITE, null); - } catch (IOException | CoreException e) { - e.printStackTrace(); - MessageDialog.openError(null, "Share Client", "Failed to update file: " + e.toString()); - return; - } - } - - if (message instanceof RequestFullSyncMessage req) { - Map paths = new HashMap<>(); - if (req.documentPath() == null) { - // Sync entire (shared) workspace - for (IProject project : activeSession.getSharedProjects()) { - var files = getProjectFiles(project); - if (files == null) return; - paths.putAll(files); - } - } else { + if (message instanceof FullSyncMessage sync) { + // TODO: handle FULL_SYNC ProjectRelativePath path; try { - path = ProjectRelativePath.of(req.documentPath()); + path = ProjectRelativePath.of(sync.documentPath()); } catch (IllegalArgumentException e) { + e.printStackTrace(); return; } - IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(path.projectName()); - if (project == null || !project.exists()) return; - if (!activeSession.getSharedProjects().contains(project)) return; + ShareClientDocumentListener listener = partListener.getListeners().get(path); - if (!path.relativePath().isEmpty()) { - Path projectLocation = project.getLocation().toPath(); - Path filePath = projectLocation.resolve(path.relativePath()).normalize(); - if (!filePath.startsWith(projectLocation)) return; - paths.put(new ProjectRelativePath(project.getName(), path.relativePath()), filePath); - } else { - // Sync entire project - var files = getProjectFiles(project); - if (files == null) return; - paths.putAll(files); + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + IWorkspaceRoot workspaceRoot = workspace.getRoot(); + IProject project = workspaceRoot.getProject(path.projectName()); + if(project == null) return; + // TODO: make sure to not overwrite existing non-shared projects + if (!project.exists()) { + IProjectDescription description = workspace.newProjectDescription(path.projectName()); + try { + project.create(description, null); + project.open(null); + } catch (CoreException e) { + e.printStackTrace(); + MessageDialog.openError(null, "Share Client", "Failed to create project: " + e.toString()); + return; + } + System.out.println("Created project " + project); + } + + Path filePath = project.getLocation().toPath().resolve(path.relativePath()); + try { + if (!Files.exists(filePath)) { + Files.createDirectories(filePath.getParent()); + Files.createFile(filePath); + } + + // TODO: update sharedDocuments + + listener.setIgnoreChanges(true); + Files.write(filePath, sync.content()); + project.refreshLocal(IResource.DEPTH_INFINITE, null); + listener.setIgnoreChanges(false); + } catch (IOException | CoreException e) { + e.printStackTrace(); + MessageDialog.openError(null, "Share Client", "Failed to update file: " + e.toString()); + return; } } - RemoteConnection connection = activeSession.getConnection(); - for (var en : paths.entrySet()) { - if(!sendFullSyncOrChecksum(connection, req.siteID(), en.getKey(), en.getValue(), false)) return; - } - } + if (message instanceof RequestFullSyncMessage req) { + Map paths = new HashMap<>(); + if (req.documentPath() == null) { + // Sync entire (shared) workspace + for (IProject project : activeSession.getSharedProjects()) { + var files = getProjectFiles(project); + System.out.println(files); + if (files == null) return; + paths.putAll(files); + } + } else { + ProjectRelativePath path; + try { + path = ProjectRelativePath.of(req.documentPath()); + } catch (IllegalArgumentException e) { + return; + } - if(message instanceof ChangeMessage change) { - Change c = change.change(); - try { - ProjectRelativePath path = ProjectRelativePath.of(c.documentPath()); - }catch(IllegalArgumentException e) { - return; + IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(path.projectName()); + if (project == null || !project.exists()) return; + if (!activeSession.getSharedProjects().contains(project)) return; + + if (!path.relativePath().isEmpty()) { + Path projectLocation = project.getLocation().toPath(); + Path filePath = projectLocation.resolve(path.relativePath()).normalize(); + if (!filePath.startsWith(projectLocation)) return; + paths.put(new ProjectRelativePath(project.getName(), path.relativePath()), filePath); + } else { + // Sync entire project + var files = getProjectFiles(project); + if (files == null) return; + paths.putAll(files); + } + } + + RemoteConnection connection = activeSession.getConnection(); + for (var en : paths.entrySet()) { + if(!sendFullSyncOrChecksum(connection, req.siteID(), en.getKey(), en.getValue(), false)) return; + } } - // TODO: insert change into document in sharedDocuments - } + if(message instanceof ChangeMessage change) { + Change c = change.change(); + ProjectRelativePath path; + try { + path = ProjectRelativePath.of(c.documentPath()); + }catch(IllegalArgumentException e) { + return; + } + + if(activeSession.getSharedDocument(path) != null) return; + + // FIXME: doesn't work + SharedDocument doc = activeSession.getOrCreateSharedDocument(path, () -> { + try { + return Files.readString(resolvePath(path)); + }catch(IOException e) { + MessageDialog.openError(null, "Share Client", "Failed to read file: " + e.toString()); + throw new RuntimeException(e); + } + }); + + doc.onMessage(message); + } + }); } public void addSharedProject(IProject project) { @@ -278,7 +307,7 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS } private boolean sendFullSyncOrChecksum(RemoteConnection connection, int siteID, ProjectRelativePath relativePath, Path filePath, boolean checksum) { - if (!Files.isRegularFile(filePath)) return false; + if (!Files.isRegularFile(filePath)) return true; try { byte[] bytes = Files.readAllBytes(filePath); @@ -297,6 +326,16 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS return true; } + private Path resolvePath(ProjectRelativePath path) { + IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(path.projectName()); + if (project == null || !project.exists()) return null; + if (!activeSession.getSharedProjects().contains(project)) return null; + Path projectLocation = project.getLocation().toPath(); + Path filePath = projectLocation.resolve(path.relativePath()).normalize(); + if (!filePath.startsWith(projectLocation)) return null; + return filePath; + } + private Map getProjectFiles(IProject project) { try { Path projectLocation = project.getLocation().toPath(); @@ -317,14 +356,6 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS @Override public void earlyStartup() { - PlatformUI.getWorkbench().addWindowListener(ShareClientWindowListener.INSTANCE); - Arrays.stream(PlatformUI.getWorkbench().getWorkbenchWindows()).forEach(w -> { - w.addPageListener(ShareClientPageListener.INSTANCE); - Arrays.stream(w.getPages()).forEach(p -> { - p.addPartListener(partListener); - Arrays.stream(p.getEditorReferences()).forEach(e -> partListener.addDocumentListener(e)); - }); - }); } } diff --git a/src/main/java/me/mrletsplay/shareclient/util/ProjectRelativePath.java b/src/main/java/me/mrletsplay/shareclient/util/ProjectRelativePath.java index bbaffbf..73e1236 100644 --- a/src/main/java/me/mrletsplay/shareclient/util/ProjectRelativePath.java +++ b/src/main/java/me/mrletsplay/shareclient/util/ProjectRelativePath.java @@ -1,7 +1,14 @@ package me.mrletsplay.shareclient.util; +import java.util.Objects; + public record ProjectRelativePath(String projectName, String relativePath) { + public ProjectRelativePath { + Objects.requireNonNull(projectName, "projectName must not be null"); + Objects.requireNonNull(relativePath, "relativePath must not be null"); + } + @Override public String toString() { return projectName + ":" + relativePath; diff --git a/src/main/java/me/mrletsplay/shareclient/util/ShareSession.java b/src/main/java/me/mrletsplay/shareclient/util/ShareSession.java index 2efb02f..e6dbca1 100644 --- a/src/main/java/me/mrletsplay/shareclient/util/ShareSession.java +++ b/src/main/java/me/mrletsplay/shareclient/util/ShareSession.java @@ -2,24 +2,35 @@ package me.mrletsplay.shareclient.util; import java.util.ArrayList; import java.util.Comparator; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.function.Supplier; import org.eclipse.core.resources.IProject; +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.swt.widgets.Display; import me.mrletsplay.shareclient.ShareClient; +import me.mrletsplay.shareclient.util.listeners.ShareClientDocumentListener; import me.mrletsplay.shareclientcore.connection.RemoteConnection; +import me.mrletsplay.shareclientcore.document.DocumentListener; +import me.mrletsplay.shareclientcore.document.SharedDocument; public class ShareSession { private RemoteConnection connection; private String sessionID; private List sharedProjects; + private Map sharedDocuments; private List peers; public ShareSession(RemoteConnection connection, String sessionID) { this.connection = connection; this.sessionID = sessionID; this.sharedProjects = new ArrayList<>(); + this.sharedDocuments = new HashMap<>(); this.peers = new ArrayList<>(); } @@ -35,6 +46,66 @@ public class ShareSession { return sharedProjects; } + public Map getSharedDocuments() { + return sharedDocuments; + } + + + public SharedDocument getSharedDocument(ProjectRelativePath path) { + return sharedDocuments.get(path); + } + + public SharedDocument getOrCreateSharedDocument(ProjectRelativePath path, Supplier initialContents) { + return sharedDocuments.computeIfAbsent(path, p -> { + SharedDocument doc = new SharedDocument(connection, path.toString(), initialContents.get()); + + doc.addListener(new DocumentListener() { + + @Override + public void onInsert(int index, char character) { + Display.getDefault().asyncExec(() -> { + ShareClientDocumentListener documentListener = ShareClient.getDefault().getPartListener().getListeners().get(path); + if(documentListener != null) { + IDocument document = documentListener.getDocument(); + + documentListener.setIgnoreChanges(true); + try { + document.replace(index, 0, String.valueOf(character)); + } catch (BadLocationException e) { + e.printStackTrace(); + // TODO: treat as inconsistency +// MessageDialog.openError(null, "Share Client", "Failed to update document: " + e.toString()); + } + documentListener.setIgnoreChanges(false); + } + }); + } + + @Override + public void onDelete(int index) { + Display.getDefault().asyncExec(() -> { + ShareClientDocumentListener documentListener = ShareClient.getDefault().getPartListener().getListeners().get(path); + if(documentListener != null) { + IDocument document = documentListener.getDocument(); + + documentListener.setIgnoreChanges(true); + try { + document.replace(index, 1, ""); + } catch (BadLocationException e) { + e.printStackTrace(); + // TODO: treat as inconsistency +// MessageDialog.openError(null, "Share Client", "Failed to update document: " + e.toString()); + } + documentListener.setIgnoreChanges(false); + } + }); + } + }); + + return doc; + }); + } + public List getPeers() { return peers; } diff --git a/src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientDocumentListener.java b/src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientDocumentListener.java new file mode 100644 index 0000000..ccbd069 --- /dev/null +++ b/src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientDocumentListener.java @@ -0,0 +1,61 @@ +package me.mrletsplay.shareclient.util.listeners; + +import org.eclipse.jface.text.DocumentEvent; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IDocumentListener; +import org.eclipse.swt.widgets.Display; + +import me.mrletsplay.shareclient.ShareClient; +import me.mrletsplay.shareclient.util.ProjectRelativePath; +import me.mrletsplay.shareclient.util.ShareSession; +import me.mrletsplay.shareclientcore.document.SharedDocument; + +public class ShareClientDocumentListener implements IDocumentListener { + + private ProjectRelativePath path; + private IDocument document; + + private boolean ignoreChanges = false; + + public ShareClientDocumentListener(ProjectRelativePath path, IDocument document) { + this.path = path; + this.document = document; + } + + public void setIgnoreChanges(boolean ignoreChanges) { + this.ignoreChanges = ignoreChanges; + } + + public boolean isIgnoreChanges() { + return ignoreChanges; + } + + @Override + public void documentAboutToBeChanged(DocumentEvent event) { + if(ignoreChanges) return; + + System.out.println("UPDATE ON THREAD " + Thread.currentThread()); + + Display.getDefault().asyncExec(() -> { + ShareSession session = ShareClient.getDefault().getActiveSession(); + if(session == null) return; + + SharedDocument doc = session.getOrCreateSharedDocument(path, () -> event.fDocument.get()); + if(event.getLength() > 0) { + doc.localDelete(event.getOffset(), event.getLength()); + } + + doc.localInsert(event.getOffset(), event.getText()); + }); + } + + @Override + public void documentChanged(DocumentEvent event) { + + } + + public IDocument getDocument() { + return document; + } + +} diff --git a/src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientPartListener.java b/src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientPartListener.java index f97aaa9..0c76057 100644 --- a/src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientPartListener.java +++ b/src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientPartListener.java @@ -6,9 +6,7 @@ import java.util.Map; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; -import org.eclipse.jface.text.DocumentEvent; import org.eclipse.jface.text.IDocument; -import org.eclipse.jface.text.IDocumentListener; import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IPartListener2; @@ -21,25 +19,11 @@ import me.mrletsplay.shareclient.util.ProjectRelativePath; public class ShareClientPartListener implements IPartListener2 { - private Map listeners = new HashMap<>(); + private Map listeners = new HashMap<>(); - private IDocumentListener createListener(ProjectRelativePath path) { + private ShareClientDocumentListener createListener(ProjectRelativePath path, IDocument document) { if(listeners.containsKey(path)) return listeners.get(path); - - IDocumentListener listener = new IDocumentListener() { - - private boolean ignoreChanges = false; - - @Override - public void documentChanged(DocumentEvent event) { - System.out.println("Change in document at " + path); - } - - @Override - public void documentAboutToBeChanged(DocumentEvent event) { - - } - }; + ShareClientDocumentListener listener = new ShareClientDocumentListener(path, document); listeners.put(path, listener); return listener; } @@ -60,8 +44,8 @@ public class ShareClientPartListener implements IPartListener2 { Path filePath = project.getLocation().toPath().relativize(file.getLocation().toPath()); ProjectRelativePath relPath = new ProjectRelativePath(project.getName(), filePath.toString()); - System.out.println(relPath); - document.addDocumentListener(createListener(relPath)); + System.out.println("Opened editor: " + relPath); + document.addDocumentListener(createListener(relPath, document)); } @Override @@ -74,4 +58,8 @@ public class ShareClientPartListener implements IPartListener2 { addDocumentListener(partRef); } + public Map getListeners() { + return listeners; + } + }