mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJPL-252323 keep line bookmarks across in-editor bulk edits
Bulk edits like in-place rename were mistaken for a file reload, which threw away the bookmark on the edited line. (cherry picked from commit 4025718fad7215393d3001044650ff3195b56000) IJ-CR-221471 GitOrigin-RevId: 0f7578e563e39adeec795201328d670049a58835
This commit is contained in:
committed by
intellij-monorepo-bot
parent
f58a1b0361
commit
ebb53806fb
+28
-15
@@ -20,8 +20,8 @@ import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.editor.Document
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import com.intellij.openapi.editor.EditorFactory
|
||||
import com.intellij.openapi.editor.event.BulkAwareDocumentListener
|
||||
import com.intellij.openapi.editor.event.DocumentEvent
|
||||
import com.intellij.openapi.editor.event.DocumentListener
|
||||
import com.intellij.openapi.editor.impl.event.DocumentEventImpl
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManager
|
||||
import com.intellij.openapi.fileEditor.FileDocumentManagerListener
|
||||
@@ -187,12 +187,29 @@ class LineBookmarkProvider(private val project: Project, coroutineScope: Corouti
|
||||
private val TreePath.asVirtualFile
|
||||
get() = TreeUtil.getLastUserObject(ProjectViewNode::class.java, this)?.virtualFile
|
||||
|
||||
private fun afterDocumentChange(document: Document, event: DocumentEvent? = null) {
|
||||
private inner class BookmarkDocumentListener : BulkAwareDocumentListener {
|
||||
override fun beforeDocumentChangeNonBulk(event: DocumentEvent): Unit = captureExpectedText(event.document)
|
||||
override fun bulkUpdateStarting(document: Document): Unit = captureExpectedText(document)
|
||||
|
||||
override fun documentChangedNonBulk(event: DocumentEvent): Unit = afterDocumentChange(event.document, event)
|
||||
override fun bulkUpdateFinished(document: Document): Unit = afterDocumentChange(document, null)
|
||||
|
||||
override fun documentChanged(event: DocumentEvent) {
|
||||
if (event.isWholeTextReplaced && event.document.isInBulkUpdate) {
|
||||
afterDocumentChange(event.document, event)
|
||||
}
|
||||
else {
|
||||
super.documentChanged(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun afterDocumentChange(document: Document, event: DocumentEvent?) {
|
||||
val file = FileDocumentManager.getInstance().getFile(document) ?: return
|
||||
if (file is LightVirtualFile) return
|
||||
val manager = BookmarksManager.getInstance(project) ?: return
|
||||
|
||||
if (event != null && (event.isWholeTextReplaced || document.isInBulkUpdate)) {
|
||||
if (event != null && event.isWholeTextReplaced) {
|
||||
if (updateBookmarksUsingDiffMapping(file, document, event, manager)) {
|
||||
return
|
||||
}
|
||||
@@ -200,11 +217,15 @@ class LineBookmarkProvider(private val project: Project, coroutineScope: Corouti
|
||||
|
||||
validateBookmarksUsingRangeMarker(file, manager)
|
||||
|
||||
if (event != null && !event.isWholeTextReplaced && !document.isInBulkUpdate) {
|
||||
if (event == null || !event.isWholeTextReplaced) {
|
||||
bookmarkEditQueue.queue(file)
|
||||
if (hasInvalidBookmarks(file, manager)) requestValidation()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasInvalidBookmarks(file: VirtualFile, manager: BookmarksManager): Boolean =
|
||||
manager.bookmarks.any { it is InvalidBookmark && it.url == file.url }
|
||||
|
||||
private fun validateBookmarksUsingRangeMarker(file: VirtualFile, manager: BookmarksManager) {
|
||||
val map = sortedMapOf<LineBookmarkImpl, Int>(compareBy { it.line })
|
||||
val set = hashSetOf<Int>()
|
||||
@@ -468,7 +489,8 @@ class LineBookmarkProvider(private val project: Project, coroutineScope: Corouti
|
||||
}
|
||||
}
|
||||
|
||||
private fun captureExpectedTextBeforeReload(file: VirtualFile, document: Document) {
|
||||
private fun captureExpectedText(document: Document) {
|
||||
val file = FileDocumentManager.getInstance().getFile(document) ?: return
|
||||
val manager = BookmarksManager.getInstance(project) ?: return
|
||||
for (bookmark in manager.bookmarks) {
|
||||
if (bookmark is LineBookmarkImpl && bookmark.file == file) {
|
||||
@@ -514,16 +536,7 @@ class LineBookmarkProvider(private val project: Project, coroutineScope: Corouti
|
||||
init {
|
||||
if (!project.isDefault) {
|
||||
val multicaster = EditorFactory.getInstance().eventMulticaster
|
||||
multicaster.addDocumentListener(object : DocumentListener {
|
||||
override fun beforeDocumentChange(event: DocumentEvent) {
|
||||
val file = FileDocumentManager.getInstance().getFile(event.document) ?: return
|
||||
captureExpectedTextBeforeReload(file, event.document)
|
||||
}
|
||||
|
||||
override fun documentChanged(event: DocumentEvent) {
|
||||
this@LineBookmarkProvider.afterDocumentChange(event.document, event)
|
||||
}
|
||||
}, project)
|
||||
multicaster.addDocumentListener(BookmarkDocumentListener(), project)
|
||||
VirtualFileManager.getInstance().addAsyncFileListenerBackgroundable({ events -> this@LineBookmarkProvider.prepareChange(events) }, project)
|
||||
project.messageBus.connect().subscribe(FileDocumentManagerListener.TOPIC, object : FileDocumentManagerListener {
|
||||
override fun beforeDocumentSaving(document: Document) {
|
||||
|
||||
@@ -29,6 +29,7 @@ import com.intellij.psi.PsiDocumentListener;
|
||||
import com.intellij.psi.PsiDocumentManager;
|
||||
import com.intellij.testFramework.LeakHunter;
|
||||
import com.intellij.testFramework.PlatformTestUtil;
|
||||
import com.intellij.util.DocumentUtil;
|
||||
import org.intellij.lang.annotations.Language;
|
||||
import org.jetbrains.annotations.NonNls;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -168,6 +169,98 @@ public class BookmarkManagerTest extends AbstractEditorTest {
|
||||
assertEquals(2, ((LineBookmark)afterRestore.getFirst()).getLine());
|
||||
}
|
||||
|
||||
public void testWholeTextReplaceInsideBulkUpdateStillReanchorsByLineText() {
|
||||
@NonNls String before = "line 0\nline 1\nbookmarked\nline 3\n";
|
||||
@NonNls String after = "inserted\nline 0\nline 1\nbookmarked\nline 3\n";
|
||||
init(before, PlainTextFileType.INSTANCE);
|
||||
Document document = getEditor().getDocument();
|
||||
|
||||
addBookmark(2);
|
||||
|
||||
WriteCommandAction.writeCommandAction(getProject()).run(
|
||||
() -> DocumentUtil.executeInBulk(document, () -> document.setText(after)));
|
||||
|
||||
List<Bookmark> result = getManager().getBookmarks();
|
||||
assertEquals("the bookmark must survive the replacement, got: " + result, 1, result.size());
|
||||
assertInstanceOf(result.getFirst(), LineBookmark.class);
|
||||
assertEquals(3, ((LineBookmark)result.getFirst()).getLine());
|
||||
assertEquals("bookmarked", result.getFirst().getAttributes().get("lineText"));
|
||||
}
|
||||
|
||||
public void testBookmarkSurvivesBulkEditOnItsLine() {
|
||||
@NonNls String text = "line 0\nline 1\noriginal\nline 3\n";
|
||||
init(text, PlainTextFileType.INSTANCE);
|
||||
Document document = getEditor().getDocument();
|
||||
|
||||
addBookmark(2);
|
||||
Bookmark bookmark = getManager().getBookmarks().getFirst();
|
||||
getManager().setType(bookmark, BookmarkType.DIGIT_4);
|
||||
|
||||
int start = document.getLineStartOffset(2);
|
||||
int end = document.getLineEndOffset(2);
|
||||
WriteCommandAction.writeCommandAction(getProject()).run(
|
||||
() -> DocumentUtil.executeInBulk(document, () -> document.replaceString(start, end, "renamed")));
|
||||
|
||||
List<Bookmark> after = getManager().getBookmarks();
|
||||
assertEquals("bulk edit must not drop the bookmark, got: " + after, 1, after.size());
|
||||
assertInstanceOf(after.getFirst(), LineBookmark.class);
|
||||
assertEquals(2, ((LineBookmark)after.getFirst()).getLine());
|
||||
assertEquals(BookmarkType.DIGIT_4, getManager().getType(after.getFirst()));
|
||||
}
|
||||
|
||||
public void testBookmarkFollowsLineShiftDoneInsideBulkUpdate() {
|
||||
@NonNls String text = "line 0\nline 1\nline 2\noriginal\nline 4\n";
|
||||
init(text, PlainTextFileType.INSTANCE);
|
||||
Document document = getEditor().getDocument();
|
||||
|
||||
addBookmark(3);
|
||||
|
||||
WriteCommandAction.writeCommandAction(getProject()).run(() -> DocumentUtil.executeInBulk(document, () -> {
|
||||
document.replaceString(document.getLineStartOffset(3), document.getLineEndOffset(3), "renamed");
|
||||
document.deleteString(document.getLineStartOffset(1), document.getLineStartOffset(2));
|
||||
}));
|
||||
|
||||
List<Bookmark> after = getManager().getBookmarks();
|
||||
assertEquals("bulk edit must not drop the bookmark, got: " + after, 1, after.size());
|
||||
assertInstanceOf(after.getFirst(), LineBookmark.class);
|
||||
assertEquals(2, ((LineBookmark)after.getFirst()).getLine());
|
||||
}
|
||||
|
||||
public void testRestoredLineTextMergesTheMemorialBackIntoTheBookmark() {
|
||||
@NonNls String text = "line 0\nline 1\noriginal\nline 3\n";
|
||||
init(text, PlainTextFileType.INSTANCE);
|
||||
Document document = getEditor().getDocument();
|
||||
|
||||
addBookmark(2);
|
||||
|
||||
bulkEditLineTo(document, 2, "renamed");
|
||||
PlatformTestUtil.waitWithEventsDispatching(
|
||||
"the memorial did not appear after the first refactoring",
|
||||
() -> getManager().getBookmarks().stream().anyMatch(InvalidBookmark.class::isInstance),
|
||||
15);
|
||||
assertEquals("expected the live bookmark and its memorial, got: " + getManager().getBookmarks(),
|
||||
2, getManager().getBookmarks().size());
|
||||
|
||||
bulkEditLineTo(document, 2, "original");
|
||||
PlatformTestUtil.waitWithEventsDispatching(
|
||||
"the memorial was not merged back into the live bookmark",
|
||||
() -> {
|
||||
List<Bookmark> bookmarks = getManager().getBookmarks();
|
||||
return bookmarks.size() == 1 && "original".equals(bookmarks.getFirst().getAttributes().get("lineText"));
|
||||
},
|
||||
15);
|
||||
|
||||
List<Bookmark> after = getManager().getBookmarks();
|
||||
assertInstanceOf(after.getFirst(), LineBookmark.class);
|
||||
assertEquals(2, ((LineBookmark)after.getFirst()).getLine());
|
||||
assertEquals("original", after.getFirst().getAttributes().get("lineText"));
|
||||
}
|
||||
|
||||
private void bulkEditLineTo(Document document, int line, @NonNls String newText) {
|
||||
WriteCommandAction.writeCommandAction(getProject()).run(() -> DocumentUtil.executeInBulk(
|
||||
document, () -> document.replaceString(document.getLineStartOffset(line), document.getLineEndOffset(line), newText)));
|
||||
}
|
||||
|
||||
public void testFirstEditLeavesOneMemorialFurtherEditsAreSilent() {
|
||||
@NonNls String text = "line 0\nline 1\noriginal\nline 3\n";
|
||||
init(text, PlainTextFileType.INSTANCE);
|
||||
|
||||
Reference in New Issue
Block a user