diff --git a/icons/bug.png b/icons/bug.png new file mode 100644 index 0000000..71bb156 Binary files /dev/null and b/icons/bug.png differ diff --git a/icons/bug@2x.png b/icons/bug@2x.png new file mode 100644 index 0000000..2ac951f Binary files /dev/null and b/icons/bug@2x.png differ diff --git a/src/main/java/me/mrletsplay/shareclient/ShareClient.java b/src/main/java/me/mrletsplay/shareclient/ShareClient.java index ae3b8f9..b86557d 100644 --- a/src/main/java/me/mrletsplay/shareclient/ShareClient.java +++ b/src/main/java/me/mrletsplay/shareclient/ShareClient.java @@ -2,7 +2,6 @@ package me.mrletsplay.shareclient; import java.io.IOException; import java.net.URI; -import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; @@ -29,6 +28,7 @@ import org.osgi.framework.BundleContext; import me.mrletsplay.shareclient.util.ChecksumUtil; import me.mrletsplay.shareclient.util.Peer; +import me.mrletsplay.shareclient.util.ProjectAndPath; import me.mrletsplay.shareclient.util.ProjectRelativePath; import me.mrletsplay.shareclient.util.ShareSession; import me.mrletsplay.shareclient.util.SharedProject; @@ -51,6 +51,7 @@ import me.mrletsplay.shareclientcore.connection.message.Message; import me.mrletsplay.shareclientcore.connection.message.PeerJoinMessage; import me.mrletsplay.shareclientcore.connection.message.PeerLeaveMessage; import me.mrletsplay.shareclientcore.connection.message.RequestFullSyncMessage; +import me.mrletsplay.shareclientcore.document.Char; import me.mrletsplay.shareclientcore.document.SharedDocument; /** @@ -230,17 +231,15 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, Di byte[] bytes = new byte[sync.content().size()]; for(int i = 0; i < bytes.length; i++) { - + bytes[i] = sync.content().get(i).value(); } - String content = sync.content().stream() - .map(c -> String.valueOf(c.value())) - .collect(Collectors.joining()); - SharedDocument sharedDocument = activeSession.getOrCreateSharedDocument(path, () -> content); - // TODO: update sharedDocuments + SharedDocument sharedDocument = activeSession.getOrCreateSharedDocument(path, null /* because the content will be replaced afterwards anyway */); + sharedDocument.clear(); + for(Char c : sync.content()) sharedDocument.getCharBag().add(c); if(listener != null) listener.setIgnoreChanges(true); - Files.write(filePath, content.getBytes(StandardCharsets.UTF_8)); // TODO: may cause problems with binary files? but they shouldn't be shared in the first place, so... + Files.write(filePath, bytes); sharedProject.getLocal().refreshLocal(IResource.DEPTH_INFINITE, null); if(listener != null) listener.setIgnoreChanges(false); } catch (IOException | CoreException e) { @@ -299,17 +298,13 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, Di return; } - if(activeSession.getSharedDocument(path) != null) return; + SharedDocument doc = activeSession.getSharedDocument(path); // Doesn't use getOrCreateSharedDocument, because it doesn't make sense to apply a change to a document that wasn't previously shared - // 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); - } - }); + if(doc == null) { + // TODO: show proper synchronization issue warning, because all received changes should be in previously shared documents + MessageDialog.openError(null, "Share Client", "Synchronization issue (received change in non-shared document)"); + return; + } doc.onMessage(message); } @@ -333,7 +328,7 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, Di for(Map.Entry en : getProjectFiles(shared).entrySet()) { activeSession.getOrCreateSharedDocument(en.getKey(), () -> { try { - return Files.readString(en.getValue(), StandardCharsets.UTF_8); + return Files.readAllBytes(en.getValue()); } catch (IOException e) { e.printStackTrace(); throw new RuntimeException(e); @@ -364,7 +359,13 @@ public class ShareClient extends AbstractUIPlugin implements MessageListener, Di return true; } - private Path resolvePath(ProjectRelativePath path) { + private ProjectAndPath resolveToProjectAndPath(ProjectRelativePath path) { + SharedProject sharedProject = activeSession.getSharedProject(path.projectName()); + if(sharedProject == null) return null; + return new ProjectAndPath(sharedProject.getLocal(), path.relativePath()); + } + + private Path resolveToPath(ProjectRelativePath path) { SharedProject sharedProject = activeSession.getSharedProject(path.projectName()); if(sharedProject == null) return null; Path projectLocation = sharedProject.getLocal().getLocation().toPath(); diff --git a/src/main/java/me/mrletsplay/shareclient/ShareClientPreferencePage.java b/src/main/java/me/mrletsplay/shareclient/ShareClientPreferencePage.java index b1738c0..6e85db3 100644 --- a/src/main/java/me/mrletsplay/shareclient/ShareClientPreferencePage.java +++ b/src/main/java/me/mrletsplay/shareclient/ShareClientPreferencePage.java @@ -21,6 +21,7 @@ public class ShareClientPreferencePage extends FieldEditorPreferencePage impleme addField(new BooleanFieldEditor(ShareClientPreferences.SHOW_CURSORS, "Show other users' cursors (TODO)", getFieldEditorParent())); addField(new BooleanFieldEditor(ShareClientPreferences.ONLY_SHARE_SOURCE_FOLDERS, "Only share files in source folders (TODO)", getFieldEditorParent())); addField(new BooleanFieldEditor(ShareClientPreferences.USE_SHARECLIENT_PROJECT_CONFIG, "Use configuration from .shareclient file in the project (TODO)", getFieldEditorParent())); + addField(new BooleanFieldEditor(ShareClientPreferences.DEBUG_MODE, "Debug mode", getFieldEditorParent())); // TODO: update view on setting change } @Override diff --git a/src/main/java/me/mrletsplay/shareclient/ShareClientPreferences.java b/src/main/java/me/mrletsplay/shareclient/ShareClientPreferences.java index 65b3ad4..72104d2 100644 --- a/src/main/java/me/mrletsplay/shareclient/ShareClientPreferences.java +++ b/src/main/java/me/mrletsplay/shareclient/ShareClientPreferences.java @@ -7,6 +7,7 @@ public class ShareClientPreferences { USERNAME = "username", SHOW_CURSORS = "showCursors", ONLY_SHARE_SOURCE_FOLDERS = "onlyShareSourceFolders", // TODO: option to only share source folders of projects? - USE_SHARECLIENT_PROJECT_CONFIG = "useShareClientConfig"; // TODO: option to enable a .shareclient project-specific config file + USE_SHARECLIENT_PROJECT_CONFIG = "useShareClientConfig", // TODO: option to enable a .shareclient project-specific config file + DEBUG_MODE = "debugMode"; } diff --git a/src/main/java/me/mrletsplay/shareclient/util/ShareSession.java b/src/main/java/me/mrletsplay/shareclient/util/ShareSession.java index 1b6f3d0..e2b15db 100644 --- a/src/main/java/me/mrletsplay/shareclient/util/ShareSession.java +++ b/src/main/java/me/mrletsplay/shareclient/util/ShareSession.java @@ -8,9 +8,13 @@ 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 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 { @@ -79,51 +83,48 @@ public class ShareSession { 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, byte character) { -// // FIXME: potential desync. If changes arrive while waiting for asyncExec to execute, it will insert at the wrong position -// Display.getDefault().asyncExec(() -> { -// ShareClientDocumentListener documentListener = ShareClient.getDefault().getPartListener().getListener(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().getListener(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); -// } -// }); -// } -// }); + SharedDocument doc = new SharedDocument(connection, path.toString(), initialContents == null ? null : initialContents.get()); + + doc.addListener(new DocumentListener() { + + @Override + public void onInsert(int index, byte character) { + // TODO: assert that we are on the UI thread + ShareClientDocumentListener documentListener = ShareClient.getDefault().getPartListener().getListener(path); + if(documentListener != null) { + IDocument document = documentListener.getDocument(); + + documentListener.setIgnoreChanges(true); + try { + document.replace(index, 0, String.valueOf((char) character)); // FIXME: dangerous! will explode on multi-byte characters + } 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) { + // TODO: assert that we are on the UI thread + ShareClientDocumentListener documentListener = ShareClient.getDefault().getPartListener().getListener(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; }); diff --git a/src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientDocumentListener.java b/src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientDocumentListener.java index 0b567af..8e1d89f 100644 --- a/src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientDocumentListener.java +++ b/src/main/java/me/mrletsplay/shareclient/util/listeners/ShareClientDocumentListener.java @@ -1,9 +1,10 @@ package me.mrletsplay.shareclient.util.listeners; +import java.nio.charset.StandardCharsets; + 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.ProjectAndPath; @@ -38,20 +39,20 @@ public class ShareClientDocumentListener implements IDocumentListener { System.out.println("UPDATE ON THREAD " + Thread.currentThread()); - Display.getDefault().asyncExec(() -> { - ShareSession session = ShareClient.getDefault().getActiveSession(); - if(session == null) return; + // FIXME: bug somewhere here, can cause discrepancies in local editor when editing while changes are coming in - SharedProject sharedProject = session.getSharedProject(path.project()); - if(sharedProject == null) return; + ShareSession session = ShareClient.getDefault().getActiveSession(); + if(session == null) return; - SharedDocument doc = session.getOrCreateSharedDocument(new ProjectRelativePath(sharedProject.getRemoteName(), path.path()), () -> event.fDocument.get()); - if(event.getLength() > 0) { - doc.localDelete(event.getOffset(), event.getLength()); - } + SharedProject sharedProject = session.getSharedProject(path.project()); + if(sharedProject == null) return; - doc.localInsert(event.getOffset(), event.getText()); - }); + SharedDocument doc = session.getOrCreateSharedDocument(new ProjectRelativePath(sharedProject.getRemoteName(), path.path()), () -> event.fDocument.get().getBytes(StandardCharsets.UTF_8)); + if(event.getLength() > 0) { + doc.localDelete(event.getOffset(), event.getLength()); + } + + doc.localInsert(event.getOffset(), event.getText()); } @Override diff --git a/src/main/java/me/mrletsplay/shareclient/views/ScrollableDialog.java b/src/main/java/me/mrletsplay/shareclient/views/ScrollableDialog.java new file mode 100644 index 0000000..81d3aec --- /dev/null +++ b/src/main/java/me/mrletsplay/shareclient/views/ScrollableDialog.java @@ -0,0 +1,43 @@ +package me.mrletsplay.shareclient.views; + +import org.eclipse.jface.dialogs.TitleAreaDialog; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +public class ScrollableDialog extends TitleAreaDialog { + + private String text; + + public ScrollableDialog(Shell parentShell, String title, String text) { + super(parentShell); + setTitle(title); + this.text = text; + } + + @Override + protected Control createDialogArea(Composite parent) { + Composite composite = (Composite) super.createDialogArea(parent); + + GridData gridData = new GridData(); + gridData.grabExcessHorizontalSpace = true; + gridData.horizontalAlignment = GridData.FILL; + gridData.grabExcessVerticalSpace = true; + gridData.verticalAlignment = GridData.FILL; + + Text scrollable = new Text(composite, SWT.BORDER | SWT.V_SCROLL); + scrollable.setLayoutData(gridData); + scrollable.setText(text); + + return composite; + } + + @Override + protected boolean isResizable() { + return true; + } + +} diff --git a/src/main/java/me/mrletsplay/shareclient/views/ShareView.java b/src/main/java/me/mrletsplay/shareclient/views/ShareView.java index 5e884e5..73e2544 100644 --- a/src/main/java/me/mrletsplay/shareclient/views/ShareView.java +++ b/src/main/java/me/mrletsplay/shareclient/views/ShareView.java @@ -31,6 +31,7 @@ import org.eclipse.ui.dialogs.PreferencesUtil; import org.eclipse.ui.part.ViewPart; import me.mrletsplay.shareclient.ShareClient; +import me.mrletsplay.shareclient.ShareClientPreferences; import me.mrletsplay.shareclient.util.ShareSession; import me.mrletsplay.shareclientcore.connection.ConnectionException; import me.mrletsplay.shareclientcore.connection.RemoteConnection; @@ -161,6 +162,33 @@ public class ShareView extends ViewPart { }; + Action debug = new Action("Debug", ImageDescriptor.createFromFile(ShareView.class, "/icons/bug.png")) { + + @Override + public void run() { + ShareSession session = ShareClient.getDefault().getActiveSession(); + if(session == null) { + MessageDialog.openInformation(null, "Debug", "No session running"); + return; + } + + StringBuilder sb = new StringBuilder(); + + session.getSharedDocuments().forEach((path, document) -> { + sb.append("--- Document: ").append(path).append('\n'); + + sb.append(document.getContentsAsString()); + + sb.append('\n'); + sb.append('\n'); + }); + +// MessageDialog.openInformation(null, "Debug Info", sb.toString()); + new ScrollableDialog(null, "Debug Info", sb.toString()).open(); + } + + }; + if(ShareClient.getDefault().getActiveSession() == null) { toolbars.add(joinSession); }else { @@ -170,6 +198,10 @@ public class ShareView extends ViewPart { toolbars.add(showSettings); + if(ShareClient.getDefault().getPreferenceStore().getBoolean(ShareClientPreferences.DEBUG_MODE)) { + toolbars.add(debug); + } + bars.updateActionBars(); }