Add basic editor sync

This commit is contained in:
MrLetsplay 2023-12-18 22:35:47 +01:00
parent 9812d9bd2f
commit e092ba0548
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
5 changed files with 282 additions and 124 deletions

View File

@ -30,6 +30,7 @@ import me.mrletsplay.shareclient.util.ChecksumUtil;
import me.mrletsplay.shareclient.util.Peer; import me.mrletsplay.shareclient.util.Peer;
import me.mrletsplay.shareclient.util.ProjectRelativePath; import me.mrletsplay.shareclient.util.ProjectRelativePath;
import me.mrletsplay.shareclient.util.ShareSession; 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.ShareClientPageListener;
import me.mrletsplay.shareclient.util.listeners.ShareClientPartListener; import me.mrletsplay.shareclient.util.listeners.ShareClientPartListener;
import me.mrletsplay.shareclient.util.listeners.ShareClientWindowListener; import me.mrletsplay.shareclient.util.listeners.ShareClientWindowListener;
@ -60,25 +61,33 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS
// The shared instance // The shared instance
private static ShareClient plugin; private static ShareClient plugin;
private ShareClientPartListener partListener = new ShareClientPartListener(); private ShareClientPartListener partListener;
private ShareView view; private ShareView view;
private ShareSession activeSession; private ShareSession activeSession;
private Map<ProjectRelativePath, SharedDocument> sharedDocuments;
public ShareClient() { public ShareClient() {
this.sharedDocuments = new HashMap<>();
} }
@Override @Override
public void start(BundleContext context) throws Exception { public void start(BundleContext context) throws Exception {
super.start(context); super.start(context);
plugin = this; plugin = this;
partListener = new ShareClientPartListener();
getPreferenceStore().setDefault(ShareClientPreferences.SERVER_URI, "ws://localhost:5473"); getPreferenceStore().setDefault(ShareClientPreferences.SERVER_URI, "ws://localhost:5473");
getPreferenceStore().setDefault(ShareClientPreferences.SHOW_CURSORS, true); getPreferenceStore().setDefault(ShareClientPreferences.SHOW_CURSORS, true);
// new ShareWSClient(URI.create("ws://localhost:5473")).connect(); // 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 @Override
@ -158,113 +167,133 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS
@Override @Override
public void onMessage(Message message) { public void onMessage(Message message) {
System.out.println("Got: " + message); Display.getDefault().asyncExec(() -> {
if (message instanceof PeerJoinMessage join) { System.out.println("Got: " + message);
activeSession.getPeers().add(new Peer(join.peerName(), join.peerSiteID())); if (message instanceof PeerJoinMessage join) {
updateView(); 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;
} }
IWorkspace workspace = ResourcesPlugin.getWorkspace(); if(message instanceof PeerLeaveMessage leave) {
IWorkspaceRoot workspaceRoot = workspace.getRoot(); activeSession.getPeers().removeIf(p -> p.siteID() == leave.peerSiteID());
IProject project = workspaceRoot.getProject(path.projectName()); updateView();
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()); if (message instanceof FullSyncMessage sync) {
try { // TODO: handle FULL_SYNC
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<ProjectRelativePath, Path> 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 {
ProjectRelativePath path; ProjectRelativePath path;
try { try {
path = ProjectRelativePath.of(req.documentPath()); path = ProjectRelativePath.of(sync.documentPath());
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
e.printStackTrace();
return; return;
} }
IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(path.projectName()); ShareClientDocumentListener listener = partListener.getListeners().get(path);
if (project == null || !project.exists()) return;
if (!activeSession.getSharedProjects().contains(project)) return;
if (!path.relativePath().isEmpty()) { IWorkspace workspace = ResourcesPlugin.getWorkspace();
Path projectLocation = project.getLocation().toPath(); IWorkspaceRoot workspaceRoot = workspace.getRoot();
Path filePath = projectLocation.resolve(path.relativePath()).normalize(); IProject project = workspaceRoot.getProject(path.projectName());
if (!filePath.startsWith(projectLocation)) return; if(project == null) return;
paths.put(new ProjectRelativePath(project.getName(), path.relativePath()), filePath); // TODO: make sure to not overwrite existing non-shared projects
} else { if (!project.exists()) {
// Sync entire project IProjectDescription description = workspace.newProjectDescription(path.projectName());
var files = getProjectFiles(project); try {
if (files == null) return; project.create(description, null);
paths.putAll(files); 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(); if (message instanceof RequestFullSyncMessage req) {
for (var en : paths.entrySet()) { Map<ProjectRelativePath, Path> paths = new HashMap<>();
if(!sendFullSyncOrChecksum(connection, req.siteID(), en.getKey(), en.getValue(), false)) return; 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) { IProject project = ResourcesPlugin.getWorkspace().getRoot().getProject(path.projectName());
Change c = change.change(); if (project == null || !project.exists()) return;
try { if (!activeSession.getSharedProjects().contains(project)) return;
ProjectRelativePath path = ProjectRelativePath.of(c.documentPath());
}catch(IllegalArgumentException e) { if (!path.relativePath().isEmpty()) {
return; 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) { 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) { 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 { try {
byte[] bytes = Files.readAllBytes(filePath); byte[] bytes = Files.readAllBytes(filePath);
@ -297,6 +326,16 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS
return true; 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<ProjectRelativePath, Path> getProjectFiles(IProject project) { private Map<ProjectRelativePath, Path> getProjectFiles(IProject project) {
try { try {
Path projectLocation = project.getLocation().toPath(); Path projectLocation = project.getLocation().toPath();
@ -317,14 +356,6 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, IS
@Override @Override
public void earlyStartup() { 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));
});
});
} }
} }

View File

@ -1,7 +1,14 @@
package me.mrletsplay.shareclient.util; package me.mrletsplay.shareclient.util;
import java.util.Objects;
public record ProjectRelativePath(String projectName, String relativePath) { 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 @Override
public String toString() { public String toString() {
return projectName + ":" + relativePath; return projectName + ":" + relativePath;

View File

@ -2,24 +2,35 @@ package me.mrletsplay.shareclient.util;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.function.Supplier;
import org.eclipse.core.resources.IProject; 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.ShareClient;
import me.mrletsplay.shareclient.util.listeners.ShareClientDocumentListener;
import me.mrletsplay.shareclientcore.connection.RemoteConnection; import me.mrletsplay.shareclientcore.connection.RemoteConnection;
import me.mrletsplay.shareclientcore.document.DocumentListener;
import me.mrletsplay.shareclientcore.document.SharedDocument;
public class ShareSession { public class ShareSession {
private RemoteConnection connection; private RemoteConnection connection;
private String sessionID; private String sessionID;
private List<IProject> sharedProjects; private List<IProject> sharedProjects;
private Map<ProjectRelativePath, SharedDocument> sharedDocuments;
private List<Peer> peers; private List<Peer> peers;
public ShareSession(RemoteConnection connection, String sessionID) { public ShareSession(RemoteConnection connection, String sessionID) {
this.connection = connection; this.connection = connection;
this.sessionID = sessionID; this.sessionID = sessionID;
this.sharedProjects = new ArrayList<>(); this.sharedProjects = new ArrayList<>();
this.sharedDocuments = new HashMap<>();
this.peers = new ArrayList<>(); this.peers = new ArrayList<>();
} }
@ -35,6 +46,66 @@ public class ShareSession {
return sharedProjects; return sharedProjects;
} }
public Map<ProjectRelativePath, SharedDocument> getSharedDocuments() {
return sharedDocuments;
}
public SharedDocument getSharedDocument(ProjectRelativePath path) {
return sharedDocuments.get(path);
}
public SharedDocument getOrCreateSharedDocument(ProjectRelativePath path, Supplier<String> 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<Peer> getPeers() { public List<Peer> getPeers() {
return peers; return peers;
} }

View File

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

View File

@ -6,9 +6,7 @@ import java.util.Map;
import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IProject;
import org.eclipse.jface.text.DocumentEvent;
import org.eclipse.jface.text.IDocument; import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentListener;
import org.eclipse.ui.IEditorInput; import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IEditorPart;
import org.eclipse.ui.IPartListener2; import org.eclipse.ui.IPartListener2;
@ -21,25 +19,11 @@ import me.mrletsplay.shareclient.util.ProjectRelativePath;
public class ShareClientPartListener implements IPartListener2 { public class ShareClientPartListener implements IPartListener2 {
private Map<ProjectRelativePath, IDocumentListener> listeners = new HashMap<>(); private Map<ProjectRelativePath, ShareClientDocumentListener> listeners = new HashMap<>();
private IDocumentListener createListener(ProjectRelativePath path) { private ShareClientDocumentListener createListener(ProjectRelativePath path, IDocument document) {
if(listeners.containsKey(path)) return listeners.get(path); if(listeners.containsKey(path)) return listeners.get(path);
ShareClientDocumentListener listener = new ShareClientDocumentListener(path, document);
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) {
}
};
listeners.put(path, listener); listeners.put(path, listener);
return listener; return listener;
} }
@ -60,8 +44,8 @@ public class ShareClientPartListener implements IPartListener2 {
Path filePath = project.getLocation().toPath().relativize(file.getLocation().toPath()); Path filePath = project.getLocation().toPath().relativize(file.getLocation().toPath());
ProjectRelativePath relPath = new ProjectRelativePath(project.getName(), filePath.toString()); ProjectRelativePath relPath = new ProjectRelativePath(project.getName(), filePath.toString());
System.out.println(relPath); System.out.println("Opened editor: " + relPath);
document.addDocumentListener(createListener(relPath)); document.addDocumentListener(createListener(relPath, document));
} }
@Override @Override
@ -74,4 +58,8 @@ public class ShareClientPartListener implements IPartListener2 {
addDocumentListener(partRef); addDocumentListener(partRef);
} }
public Map<ProjectRelativePath, ShareClientDocumentListener> getListeners() {
return listeners;
}
} }