From bbe9991f6af927829ee1bc0774faa791d6087d35 Mon Sep 17 00:00:00 2001 From: Liana Bakradze Date: Thu, 28 Aug 2014 14:24:18 +0400 Subject: [PATCH] add offset listeners --- .../CCEditorFactoryListener.java | 47 ++++++++++- .../coursecreator/CCProjectComponent.java | 5 +- .../coursecreator/CCProjectService.java | 21 +++++ .../coursecreator/StudyDocumentListener.java | 71 ++++++++++++++++ .../actions/CreateCourseArchive.java | 69 +++++++++++---- .../coursecreator/format/TaskFile.java | 84 ++++++++++++++++++- .../coursecreator/format/TaskWindow.java | 53 +++++++++++- 7 files changed, 325 insertions(+), 25 deletions(-) create mode 100644 src/org/jetbrains/plugins/coursecreator/StudyDocumentListener.java diff --git a/src/org/jetbrains/plugins/coursecreator/CCEditorFactoryListener.java b/src/org/jetbrains/plugins/coursecreator/CCEditorFactoryListener.java index 6fd8d9316e72..1eb8690aad22 100644 --- a/src/org/jetbrains/plugins/coursecreator/CCEditorFactoryListener.java +++ b/src/org/jetbrains/plugins/coursecreator/CCEditorFactoryListener.java @@ -1,5 +1,6 @@ package org.jetbrains.plugins.coursecreator; +import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.event.EditorFactoryEvent; import com.intellij.openapi.editor.event.EditorFactoryListener; @@ -7,7 +8,7 @@ import com.intellij.openapi.fileEditor.FileDocumentManager; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.annotations.NotNull; -import org.jetbrains.plugins.coursecreator.format.Course; +import org.jetbrains.plugins.coursecreator.format.*; public class CCEditorFactoryListener implements EditorFactoryListener { @Override @@ -25,11 +26,55 @@ public class CCEditorFactoryListener implements EditorFactoryListener { if (course == null) { return; } + final VirtualFile taskDir = virtualFile.getParent(); + if (taskDir == null || !taskDir.getName().contains("task")) { + return; + } + final VirtualFile lessonDir = taskDir.getParent(); + if (lessonDir == null) return; + final Lesson lesson = course.getLesson(lessonDir.getName()); + final Task task = lesson.getTask(taskDir.getName()); + final TaskFile taskFile = task.getTaskFile(virtualFile.getName()); + TaskFileModificationListener listener = new TaskFileModificationListener(taskFile); + CCProjectService.addDocumentListener(editor.getDocument(), listener); + editor.getDocument().addDocumentListener(listener); CCProjectService.drawTaskWindows(virtualFile, editor, course); } @Override public void editorReleased(@NotNull EditorFactoryEvent event) { + Editor editor = event.getEditor(); + Document document = editor.getDocument(); + StudyDocumentListener listener = CCProjectService.getListener(document); + if (listener != null) { + document.removeDocumentListener(listener); + CCProjectService.removeListener(document); + } + editor.getMarkupModel().removeAllHighlighters(); + editor.getSelectionModel().removeSelection(); + } + private class TaskFileModificationListener extends StudyDocumentListener { + + private final TaskFile myTaskFile; + + public TaskFileModificationListener(TaskFile taskFile) { + super(taskFile); + myTaskFile = taskFile; + } + + @Override + protected void updateTaskWindowLength(CharSequence fragment, TaskWindow taskWindow, int change) { + int newLength = taskWindow.getReplacementLength() + change; + taskWindow.setReplacementLength(newLength <= 0 ? 0 : newLength); + if (fragment.equals("\n")) { + taskWindow.setReplacementLength(taskWindow.getLength() + 1); + } + } + + @Override + protected boolean needModify() { + return myTaskFile.isTrackChanges(); + } } } diff --git a/src/org/jetbrains/plugins/coursecreator/CCProjectComponent.java b/src/org/jetbrains/plugins/coursecreator/CCProjectComponent.java index b3a7af9d0ab5..34de943d2ad0 100644 --- a/src/org/jetbrains/plugins/coursecreator/CCProjectComponent.java +++ b/src/org/jetbrains/plugins/coursecreator/CCProjectComponent.java @@ -3,10 +3,13 @@ package org.jetbrains.plugins.coursecreator; import com.intellij.openapi.components.ProjectComponent; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.EditorFactory; +import com.intellij.openapi.editor.event.EditorFactoryEvent; +import com.intellij.openapi.editor.impl.EditorFactoryImpl; import com.intellij.openapi.fileEditor.FileEditor; import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.fileEditor.impl.text.PsiAwareTextEditorImpl; import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; import com.intellij.openapi.startup.StartupManager; import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.annotations.NotNull; @@ -42,7 +45,7 @@ public class CCProjectComponent implements ProjectComponent { FileEditor fileEditor = FileEditorManager.getInstance(myProject).getSelectedEditor(file); if (fileEditor instanceof PsiAwareTextEditorImpl) { Editor editor = ((PsiAwareTextEditorImpl)fileEditor).getEditor(); - CCProjectService.drawTaskWindows(file, editor, course); + new CCEditorFactoryListener().editorCreated(new EditorFactoryEvent(new EditorFactoryImpl(ProjectManager.getInstance()), editor )); } } } diff --git a/src/org/jetbrains/plugins/coursecreator/CCProjectService.java b/src/org/jetbrains/plugins/coursecreator/CCProjectService.java index d9ff744bdccb..1e38bab865cb 100644 --- a/src/org/jetbrains/plugins/coursecreator/CCProjectService.java +++ b/src/org/jetbrains/plugins/coursecreator/CCProjectService.java @@ -21,6 +21,7 @@ import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; @@ -31,7 +32,9 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.plugins.coursecreator.format.*; import java.io.File; +import java.util.HashMap; import java.util.List; +import java.util.Map; @State(name = "CCProjectService", storages = { @@ -43,6 +46,7 @@ public class CCProjectService implements PersistentStateComponent { private static final Logger LOG = Logger.getInstance(CCProjectService.class.getName()); public Course myCourse; public static final String COURSE_ELEMENT = "course"; + private static final Map myDocumentListeners = new HashMap(); public void setCourse(@NotNull final Course course) { myCourse = course; @@ -114,4 +118,21 @@ public class CCProjectService implements PersistentStateComponent { taskWindow.drawHighlighter(editor); } } + + public static void addDocumentListener(Document document, StudyDocumentListener listener) { + myDocumentListeners.put(document, listener); + } + + public static StudyDocumentListener getListener(Document document) { + return myDocumentListeners.get(document); + } + + public static void removeListener(Document document) { + myDocumentListeners.remove(document); + } + + public static boolean indexIsValid(int index, List collection) { + int size = collection.size(); + return index >= 0 && index < size; + } } diff --git a/src/org/jetbrains/plugins/coursecreator/StudyDocumentListener.java b/src/org/jetbrains/plugins/coursecreator/StudyDocumentListener.java new file mode 100644 index 000000000000..d803e0e8fd97 --- /dev/null +++ b/src/org/jetbrains/plugins/coursecreator/StudyDocumentListener.java @@ -0,0 +1,71 @@ +package org.jetbrains.plugins.coursecreator; + +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.LogicalPosition; +import com.intellij.openapi.editor.event.DocumentAdapter; +import com.intellij.openapi.editor.event.DocumentEvent; +import com.intellij.openapi.editor.impl.event.DocumentEventImpl; +import org.jetbrains.plugins.coursecreator.format.TaskFile; +import org.jetbrains.plugins.coursecreator.format.TaskWindow; + +/** + * author: liana + * data: 7/16/14. + * Listens changes in study files and updates + * coordinates of all the windows in current task file + */ +public abstract class StudyDocumentListener extends DocumentAdapter { + private final TaskFile myTaskFile; + private int oldLine; + private int oldLineStartOffset; + private TaskWindow myTaskWindow; + + public StudyDocumentListener(TaskFile taskFile) { + myTaskFile = taskFile; + } + + + //remembering old end before document change because of problems + // with fragments containing "\n" + @Override + public void beforeDocumentChange(DocumentEvent e) { + int offset = e.getOffset(); + int oldEnd = offset + e.getOldLength(); + Document document = e.getDocument(); + oldLine = document.getLineNumber(oldEnd); + oldLineStartOffset = document.getLineStartOffset(oldLine); + int line = document.getLineNumber(offset); + int offsetInLine = offset - document.getLineStartOffset(line); + LogicalPosition pos = new LogicalPosition(line, offsetInLine); + myTaskWindow = myTaskFile.getTaskWindow(document, pos); + + } + + @Override + public void documentChanged(DocumentEvent e) { + if (e instanceof DocumentEventImpl) { + if (!needModify()) { + return; + } + DocumentEventImpl event = (DocumentEventImpl)e; + Document document = e.getDocument(); + int offset = e.getOffset(); + int change = event.getNewLength() - event.getOldLength(); + if (myTaskWindow != null) { + updateTaskWindowLength(e.getNewFragment(), myTaskWindow, change); + } + int newEnd = offset + event.getNewLength(); + int newLine = document.getLineNumber(newEnd); + int lineChange = newLine - oldLine; + myTaskFile.incrementLines(oldLine + 1, lineChange); + int newEndOffsetInLine = offset + e.getNewLength() - document.getLineStartOffset(newLine); + int oldEndOffsetInLine = offset + e.getOldLength() - oldLineStartOffset; + myTaskFile.updateLine(lineChange, oldLine, newEndOffsetInLine, oldEndOffsetInLine); + } + } + + protected abstract void updateTaskWindowLength(CharSequence fragment, TaskWindow taskWindow, int change); + + protected abstract boolean needModify(); +} + diff --git a/src/org/jetbrains/plugins/coursecreator/actions/CreateCourseArchive.java b/src/org/jetbrains/plugins/coursecreator/actions/CreateCourseArchive.java index 76a8fdb6724c..05428f4e82d1 100644 --- a/src/org/jetbrains/plugins/coursecreator/actions/CreateCourseArchive.java +++ b/src/org/jetbrains/plugins/coursecreator/actions/CreateCourseArchive.java @@ -8,12 +8,9 @@ import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.command.CommandProcessor; -import com.intellij.openapi.command.undo.UndoManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.editor.Document; import com.intellij.openapi.fileEditor.FileDocumentManager; -import com.intellij.openapi.fileEditor.FileEditor; -import com.intellij.openapi.fileEditor.FileEditorManager; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; @@ -22,13 +19,12 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.openapi.vfs.VirtualFileManager; import com.intellij.util.io.ZipUtil; import org.jetbrains.plugins.coursecreator.CCProjectService; +import org.jetbrains.plugins.coursecreator.StudyDocumentListener; import org.jetbrains.plugins.coursecreator.format.*; import org.jetbrains.plugins.coursecreator.ui.CreateCourseArchiveDialog; import java.io.*; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.zip.ZipOutputStream; public class CreateCourseArchive extends DumbAwareAction { @@ -57,7 +53,6 @@ public class CreateCourseArchive extends DumbAwareAction { final CCProjectService service = CCProjectService.getInstance(project); final Course course = service.getCourse(); if (course == null) return; - generateJson(project); CreateCourseArchiveDialog dlg = new CreateCourseArchiveDialog(project, this); dlg.show(); if (dlg.getExitCode() != DialogWrapper.OK_EXIT_CODE) { @@ -65,7 +60,8 @@ public class CreateCourseArchive extends DumbAwareAction { } final VirtualFile baseDir = project.getBaseDir(); final Map lessons = course.getLessonsMap(); - List editorList = new ArrayList(); + //List editorList = new ArrayList(); + Map taskFiles = new HashMap(); for (Map.Entry lesson : lessons.entrySet()) { final VirtualFile lessonDir = baseDir.findChild(lesson.getKey()); if (lessonDir == null) continue; @@ -77,17 +73,16 @@ public class CreateCourseArchive extends DumbAwareAction { if (file == null) continue; final Document document = FileDocumentManager.getInstance().getDocument(file); if (document == null) continue; - final TaskFile taskFile = entry.getValue(); - for (final TaskWindow taskWindow : taskFile.getTaskWindows()) { + document.addDocumentListener(new InsertionListener(taskFile)); + taskFiles.put(file, taskFile); + taskFile.setTrackChanges(false); + Collections.sort(taskFile.getTaskWindows()); + for (int i = taskFile.getTaskWindows().size() - 1; i >=0 ; i--) { + final TaskWindow taskWindow = taskFile.getTaskWindows().get(i); final String taskText = taskWindow.getTaskText(); final int lineStartOffset = document.getLineStartOffset(taskWindow.line); final int offset = lineStartOffset + taskWindow.start; - final FileEditor[] editors = FileEditorManager.getInstance(project).getEditors(file); - if (editors.length > 0) { - editorList.add(editors[0]); - } - CommandProcessor.getInstance().executeCommand(project, new Runnable() { @Override public void run() { @@ -104,6 +99,7 @@ public class CreateCourseArchive extends DumbAwareAction { } } } + generateJson(project); try { File zipFile = new File(myLocationDir, myZipName + ".zip"); ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipFile))); @@ -124,10 +120,30 @@ public class CreateCourseArchive extends DumbAwareAction { LOG.error(e1); } - for (FileEditor fileEditor : editorList) { - UndoManager.getInstance(project).undo(fileEditor); + for (Map.Entry entry: taskFiles.entrySet()) { + TaskFile value = entry.getValue(); + final Document document = FileDocumentManager.getInstance().getDocument(entry.getKey()); + if (document == null) { + continue; + } + for (final TaskWindow taskWindow : value.getTaskWindows()){ + final int lineStartOffset = document.getLineStartOffset(taskWindow.line); + final int offset = lineStartOffset + taskWindow.start; + CommandProcessor.getInstance().executeCommand(project, new Runnable() { + @Override + public void run() { + ApplicationManager.getApplication().runWriteAction(new Runnable() { + @Override + public void run() { + document.replaceString(offset, offset + taskWindow.length, taskWindow.getPossibleAnswer()); + FileDocumentManager.getInstance().saveDocument(document); + } + }); + } + }, "x", "qwe"); + } + value.setTrackChanges(true); } - VirtualFileManager.getInstance().refreshWithoutFileWatcher(true); ProjectView.getInstance(project).refresh(); } @@ -162,4 +178,21 @@ public class CreateCourseArchive extends DumbAwareAction { } } } + + private class InsertionListener extends StudyDocumentListener { + + public InsertionListener(TaskFile taskFile) { + super(taskFile); + } + + @Override + protected void updateTaskWindowLength(CharSequence fragment, TaskWindow taskWindow, int change) { + //we don't need to update task window length + } + + @Override + protected boolean needModify() { + return true; + } + } } \ No newline at end of file diff --git a/src/org/jetbrains/plugins/coursecreator/format/TaskFile.java b/src/org/jetbrains/plugins/coursecreator/format/TaskFile.java index 79f94d711143..85f0d91983f2 100644 --- a/src/org/jetbrains/plugins/coursecreator/format/TaskFile.java +++ b/src/org/jetbrains/plugins/coursecreator/format/TaskFile.java @@ -1,7 +1,11 @@ package org.jetbrains.plugins.coursecreator.format; import com.google.gson.annotations.Expose; +import com.intellij.openapi.editor.Document; +import com.intellij.openapi.editor.LogicalPosition; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.plugins.coursecreator.CCProjectService; import java.util.ArrayList; import java.util.List; @@ -9,6 +13,15 @@ import java.util.List; public class TaskFile { @Expose public List task_windows = new ArrayList(); public int myIndex; + public boolean myTrackChanges = true; + + public boolean isTrackChanges() { + return myTrackChanges; + } + + public void setTrackChanges(boolean trackChanges) { + myTrackChanges = trackChanges; + } public TaskFile() {} @@ -25,7 +38,74 @@ public class TaskFile { myIndex = index; } - public int getIndex() { - return myIndex; + + /** + * @param pos position in editor + * @return task window located in specified position or null if there is no task window in this position + */ + @Nullable + public TaskWindow getTaskWindow(@NotNull final Document document, @NotNull final LogicalPosition pos) { + int line = pos.line; + if (line >= document.getLineCount()) { + return null; + } + int column = pos.column; + int offset = document.getLineStartOffset(line) + column; + for (TaskWindow tw : task_windows) { + if (tw.getLine() <= line) { + int twStartOffset = tw.getRealStartOffset(document); + final int length = tw.getReplacementLength() > 0 ? tw.getReplacementLength() : 0; + int twEndOffset = twStartOffset + length; + if (twStartOffset <= offset && offset <= twEndOffset) { + return tw; + } + } + } + return null; + } + + /** + * Updates task window lines + * + * @param startLine lines greater than this line and including this line will be updated + * @param change change to be added to line numbers + */ + public void incrementLines(int startLine, int change) { + for (TaskWindow taskTaskWindow : task_windows) { + if (taskTaskWindow.getLine() >= startLine) { + taskTaskWindow.setLine(taskTaskWindow.getLine() + change); + } + } + } + + /** + * Updates windows in specific line + * + * @param lineChange change in line number + * @param line line to be updated + * @param newEndOffsetInLine distance from line start to end of inserted fragment + * @param oldEndOffsetInLine distance from line start to end of changed fragment + */ + public void updateLine(int lineChange, int line, int newEndOffsetInLine, int oldEndOffsetInLine) { + for (TaskWindow w : task_windows) { + if ((w.getLine() == line) && (w.getStart() >= oldEndOffsetInLine)) { + int distance = w.getStart() - oldEndOffsetInLine; + boolean coveredByPrevTW = false; + int prevIndex = w.getIndex() - 1; + if (CCProjectService.indexIsValid(prevIndex, task_windows)) { + TaskWindow prevTW = task_windows.get(prevIndex); + if (prevTW.getLine() == line) { + int endOffset = prevTW.getStart() + prevTW.getLength(); + if (endOffset >= newEndOffsetInLine) { + coveredByPrevTW = true; + } + } + } + if (lineChange != 0 || newEndOffsetInLine <= w.getStart() || coveredByPrevTW) { + w.setStart(distance + newEndOffsetInLine); + w.setLine(line + lineChange); + } + } + } } } diff --git a/src/org/jetbrains/plugins/coursecreator/format/TaskWindow.java b/src/org/jetbrains/plugins/coursecreator/format/TaskWindow.java index 92c265ed8e9f..cb6418ec75d7 100644 --- a/src/org/jetbrains/plugins/coursecreator/format/TaskWindow.java +++ b/src/org/jetbrains/plugins/coursecreator/format/TaskWindow.java @@ -1,11 +1,13 @@ package org.jetbrains.plugins.coursecreator.format; import com.google.gson.annotations.Expose; +import com.intellij.openapi.editor.Document; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.editor.colors.EditorColors; import com.intellij.openapi.editor.colors.EditorColorsManager; import com.intellij.openapi.editor.markup.HighlighterLayer; import com.intellij.openapi.editor.markup.HighlighterTargetArea; +import com.intellij.openapi.editor.markup.RangeHighlighter; import com.intellij.openapi.editor.markup.TextAttributes; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; @@ -14,7 +16,7 @@ import org.jetbrains.plugins.coursecreator.CCProjectService; import java.io.File; -public class TaskWindow { +public class TaskWindow implements Comparable{ @Expose public int line; @Expose public int start; @@ -71,8 +73,11 @@ public class TaskWindow { int endOffset = startOffset + myReplacementLength; TextAttributes defaultTestAttributes = EditorColorsManager.getInstance().getGlobalScheme().getAttributes(EditorColors.LIVE_TEMPLATE_ATTRIBUTES); - editor.getMarkupModel().addRangeHighlighter(startOffset, endOffset, HighlighterLayer.LAST + 1, defaultTestAttributes, - HighlighterTargetArea.EXACT_RANGE); + RangeHighlighter highlighter = + editor.getMarkupModel().addRangeHighlighter(startOffset, endOffset, HighlighterLayer.LAST + 1, defaultTestAttributes, + HighlighterTargetArea.EXACT_RANGE); + highlighter.setGreedyToLeft(true); + highlighter.setGreedyToRight(true); } public int getIndex() { @@ -83,4 +88,46 @@ public class TaskWindow { myIndex = index; } + + public void setReplacementLength(int replacementLength) { + myReplacementLength = replacementLength; + } + + public int getLine() { + return line; + } + + public int getRealStartOffset(Document document) { + return document.getLineStartOffset(line) + start; + } + + public void setLine(int line) { + this.line = line; + } + + public int getStart() { + return start; + } + + public void setStart(int start) { + this.start = start; + } + + @Override + public int compareTo(Object o) { + TaskWindow taskWindow = (TaskWindow)o; + int lineDiff = line - taskWindow.line; + if (lineDiff == 0) { + return start - taskWindow.start; + } + return lineDiff; + } + + public String getPossibleAnswer() { + return possible_answer; + } + + public int getLength() { + return length; + } } \ No newline at end of file