mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-18 09:34:34 +07:00
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:
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user