diff --git a/platform/lang-impl/src/com/intellij/internal/RetypeFileAction.kt b/platform/lang-impl/src/com/intellij/internal/RetypeFileAction.kt index b19306286453..4f2bebd6d8ec 100644 --- a/platform/lang-impl/src/com/intellij/internal/RetypeFileAction.kt +++ b/platform/lang-impl/src/com/intellij/internal/RetypeFileAction.kt @@ -1,8 +1,11 @@ // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. package com.intellij.internal +import com.intellij.codeInsight.CodeInsightSettings +import com.intellij.codeInsight.lookup.LookupElement import com.intellij.codeInsight.lookup.LookupElementPresentation import com.intellij.codeInsight.lookup.LookupManager +import com.intellij.codeInsight.lookup.impl.LookupImpl import com.intellij.codeInsight.template.impl.LiveTemplateLookupElement import com.intellij.diagnostic.ThreadDumper import com.intellij.ide.DataManager @@ -15,8 +18,12 @@ import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.IdeActions import com.intellij.openapi.actionSystem.ex.ActionManagerEx import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.LogicalPosition import com.intellij.openapi.editor.actionSystem.LatencyRecorder import com.intellij.openapi.editor.impl.EditorImpl +import com.intellij.openapi.extensions.ExtensionPointName +import com.intellij.openapi.extensions.Extensions import com.intellij.openapi.project.Project import com.intellij.openapi.ui.Messages import com.intellij.openapi.util.Disposer @@ -54,95 +61,151 @@ class RetypeFileAction : AnAction() { } } +interface RetypeFileAssistant { + fun acceptLookupElement(element: LookupElement): Boolean + + companion object { + val EP_NAME = ExtensionPointName.create("com.intellij.retypeFileAssistant") + } +} + class RetypeSession(private val project: Project, private val editor: EditorImpl, private val delayMillis: Int) : Disposable { + private val document = editor.document private val alarm = Alarm(Alarm.ThreadToUse.SWING_THREAD, this) private val threadDumpAlarm = Alarm(Alarm.ThreadToUse.POOLED_THREAD, this) - private val text = editor.document.text - private var pos = 0 + private val originalText = editor.document.text + private val lines = editor.document.text.split('\n').map { it + "\n" } + private var line = -1 + private var column = 0 private var completedChars = 0 private val threadDumps = mutableListOf() + private val oldSelectAutopopup = CodeInsightSettings.getInstance().SELECT_AUTOPOPUP_SUGGESTIONS_BY_CHARS + private var needSyncPosition = false + private var editorLineBeforeAcceptingLookup = -1 + + val currentLineText get() = lines[line] fun start() { latencyMap.clear() - WriteCommandAction.runWriteCommandAction(project) { editor.document.deleteString(0, editor.document.textLength) } + WriteCommandAction.runWriteCommandAction(project) { document.deleteString(0, document.textLength) } + CodeInsightSettings.getInstance().SELECT_AUTOPOPUP_SUGGESTIONS_BY_CHARS = false queueNext() } fun stop() { - WriteCommandAction.runWriteCommandAction(project) { editor.document.replaceString(0, editor.document.textLength, text) } + WriteCommandAction.runWriteCommandAction(project) { document.replaceString(0, document.textLength, originalText) } Disposer.dispose(this) editor.putUserData(RETYPE_SESSION_KEY, null) + CodeInsightSettings.getInstance().SELECT_AUTOPOPUP_SUGGESTIONS_BY_CHARS = oldSelectAutopopup } override fun dispose() { } private fun queueNext() { - alarm.addRequest(Runnable { typeNext() }, delayMillis) + if (!alarm.isDisposed) { + alarm.addRequest({ typeNext() }, delayMillis) + } } private fun typeNext() { - threadDumpAlarm.addRequest(Runnable { logThreadDump() }, 100) + threadDumpAlarm.addRequest({ logThreadDump() }, 100) - val lookup = LookupManager.getActiveLookup(editor) + if (column == 0) { + if (line >= 0) { + if (checkPrevLineInSync()) return + } + line++ + syncPositionWithEditor() + } + else if (needSyncPosition) { + val lineDelta = editor.caretModel.logicalPosition.line - editorLineBeforeAcceptingLookup + if (lineDelta > 0) { + line += lineDelta + column = 0 + } + syncPositionWithEditor() + } + needSyncPosition = false + + val lookup = LookupManager.getActiveLookup(editor) as LookupImpl? var lookupSelected = false if (lookup != null) { - val lookupString = lookup.currentItem?.let { LookupElementPresentation.renderElement(it).itemText} - if (lookupString != null && lookup.currentItem !is LiveTemplateLookupElement && - text.substring(lookup.lookupStart, lookup.lookupStart + lookupString.length) == lookupString) { + val lookupString = lookup.currentItem?.let { LookupElementPresentation.renderElement(it).itemText } + val lookupStartColumn = editor.offsetToLogicalPosition(lookup.lookupStart).column + if (lookupString != null && isLookupElementAcceptable(lookup.currentItem) && + currentLineText.drop(lookupStartColumn).take(lookupString.length) == lookupString) { + lookup.focusDegree = LookupImpl.FocusDegree.FOCUSED + editorLineBeforeAcceptingLookup = editor.caretModel.logicalPosition.line executeEditorAction(IdeActions.ACTION_CHOOSE_LOOKUP_ITEM) - syncPositionWithEditor() + needSyncPosition = true lookupSelected = true } } + if (!lookupSelected) { - val c = text[pos] + val c = currentLineText[column] if (c == '\n') { - // catch up with previously triggered typed handlers - if (syncPositionWithEditor()) { - queueNext() - return - } - pos++ + column = 0 // line will be incremented in next loop executeEditorAction(IdeActions.ACTION_EDITOR_ENTER) - syncPositionWithEditor() } else { - pos++ + column++ editor.type(c.toString()) } } - if (pos < text.length) { + if ((column == 0 && line < lines.size - 1) || (column > 0 && column < currentLineText.length)) { queueNext() } else { stop() - if (editor.document.text != text) { - Messages.showErrorDialog(project, "Retype failed, editor text differs", "Retype") - } - else { - TypingLatencyReportDialog(project, threadDumps).show() + TypingLatencyReportDialog(project, threadDumps).show() + } + } + + private fun isLookupElementAcceptable(lookupElement: LookupElement?): Boolean { + if (lookupElement == null) return false + for (retypeFileAssistant in Extensions.getExtensions(RetypeFileAssistant.EP_NAME)) { + if (!retypeFileAssistant.acceptLookupElement(lookupElement)) { + return false } } + return lookupElement !is LiveTemplateLookupElement + } + + private fun checkPrevLineInSync(): Boolean { + val prevLine = getEditorLineText(editor.caretModel.logicalPosition.line - 1) + if (prevLine.trimEnd() != currentLineText.trimEnd()) { + Messages.showErrorDialog(project, "Text has diverged. Expected:\n$currentLineText\nActual:\n$prevLine", "Retype File") + stop() + return true + } + return false + } + + private fun getEditorLineText(line: Int): String { + val prevLineStart = document.getLineStartOffset(line) + val prevLineEnd = document.getLineEndOffset(line) + return document.text.substring(prevLineStart, prevLineEnd) } private fun syncPositionWithEditor(): Boolean { var result = false - while (pos < editor.document.text.length && pos < text.length && editor.document.charsSequence[pos] == text[pos]) { + val editorLine = editor.caretModel.logicalPosition.line + val editorLineText = getEditorLineText(editorLine) + while (column < editorLineText.length && column < currentLineText.length && editorLineText[column] == currentLineText[column]) { result = true completedChars++ - pos++ + column++ } - if (editor.caretModel.offset <= pos) { - editor.caretModel.moveToOffset(pos) + if (editor.caretModel.logicalPosition.column < column) { + editor.caretModel.moveToLogicalPosition(LogicalPosition(line, column)) } - else { + else if (editor.caretModel.logicalPosition.column > column) { // unwanted completion, backtrack - println("Text has diverged, backtracking. Editor text:\n${editor.document.text}\nBuffer text:\n${text.substring(0, pos)}") + println("Text has diverged, backtracking. Editor text:\n$editorLineText\nBuffer text:\n$currentLineText") WriteCommandAction.runWriteCommandAction(project) { - while (editor.caretModel.offset > pos) { - executeEditorAction(IdeActions.ACTION_EDITOR_BACKSPACE) - } + editor.document.deleteString(document.getLineStartOffset(editorLine) + column, document.getLineEndOffset(editorLine)) } } return result @@ -162,9 +225,13 @@ class RetypeSession(private val project: Project, private val editor: EditorImpl private fun logThreadDump() { if (editor.isProcessingTypedAction) { threadDumps.add(ThreadDumper.dumpThreadsToString()) - threadDumpAlarm.addRequest(Runnable { logThreadDump() }, 100) + threadDumpAlarm.addRequest({ logThreadDump() }, 100) } } + + companion object { + val LOG = Logger.getInstance("#com.intellij.internal.RetypeSesssion") + } } val RETYPE_SESSION_KEY = Key.create("com.intellij.internal.RetypeSession") diff --git a/platform/platform-resources/src/META-INF/LangExtensionPoints.xml b/platform/platform-resources/src/META-INF/LangExtensionPoints.xml index ac0cc69e3409..d4214f0f4154 100644 --- a/platform/platform-resources/src/META-INF/LangExtensionPoints.xml +++ b/platform/platform-resources/src/META-INF/LangExtensionPoints.xml @@ -969,6 +969,8 @@ interface="com.intellij.execution.update.RunningApplicationUpdaterProvider"/> + +