Add basic editor sync
This commit is contained in:
parent
9812d9bd2f
commit
e092ba0548
@ -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<ProjectRelativePath, SharedDocument> 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<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 {
|
||||
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<ProjectRelativePath, Path> 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<ProjectRelativePath, Path> 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));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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<IProject> sharedProjects;
|
||||
private Map<ProjectRelativePath, SharedDocument> sharedDocuments;
|
||||
private List<Peer> 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<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() {
|
||||
return peers;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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<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);
|
||||
|
||||
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<ProjectRelativePath, ShareClientDocumentListener> getListeners() {
|
||||
return listeners;
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user