Rider-friendly implementation of Retype File action

Deal with typed handlers executed asynchronously. Synchronize position on line-by-line basis to deal with trailing
spaces differences and typed handlers reformatting already entered contents of the file. Don't automatically
accept completion suggestions. Add extension point to exclude Rider's live template lookup items from being accepted.
This commit is contained in:
Dmitry Jemerov
2018-07-09 19:21:25 +02:00
parent d585ccb554
commit d8a462b31f
2 changed files with 105 additions and 36 deletions
@@ -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<RetypeFileAssistant>("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<String>()
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<RetypeSession>("com.intellij.internal.RetypeSession")
@@ -969,6 +969,8 @@
interface="com.intellij.execution.update.RunningApplicationUpdaterProvider"/>
<extensionPoint name="languageCompilerRefAdapter" interface="com.intellij.compiler.backwardRefs.LanguageCompilerRefAdapter"/>
<extensionPoint name="retypeFileAssistant" interface="com.intellij.internal.RetypeFileAssistant"/>
</extensionPoints>
</idea-plugin>