mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Retype: better results presentation, fixed a couple more desync cases, add failing test
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
class Braces {
|
||||
void bar() {
|
||||
|
||||
}
|
||||
|
||||
void baz() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
public class Import {
|
||||
public void foo() {
|
||||
Arrays.asList();
|
||||
}
|
||||
|
||||
private static class Arrays {
|
||||
public static void asList() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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.retype
|
||||
|
||||
import com.intellij.JavaTestUtil
|
||||
import com.intellij.ide.IdeEventQueue
|
||||
import com.intellij.openapi.editor.impl.EditorImpl
|
||||
import com.intellij.testFramework.fixtures.LightCodeInsightFixtureTestCase
|
||||
|
||||
/**
|
||||
* @author yole
|
||||
*/
|
||||
abstract class JavaRetypeTest : LightCodeInsightFixtureTestCase() {
|
||||
override fun getBasePath(): String = JavaTestUtil.getRelativeJavaTestDataPath()
|
||||
|
||||
fun _testBraces() {
|
||||
doTest()
|
||||
}
|
||||
|
||||
fun _testImport() {
|
||||
doTest()
|
||||
}
|
||||
|
||||
private fun doTest() {
|
||||
val filePath = "/retype/${getTestName(false)}.java"
|
||||
myFixture.configureByFile(filePath)
|
||||
RetypeSession(project, myFixture.editor as EditorImpl, 0, 0).start()
|
||||
while (editor.getUserData(RETYPE_SESSION_KEY) != null) {
|
||||
IdeEventQueue.getInstance().flushQueue()
|
||||
}
|
||||
myFixture.checkResultByFile(filePath)
|
||||
}
|
||||
}
|
||||
+17
-21
@@ -36,7 +36,6 @@ class TypingLatencyReportAction : AnAction() {
|
||||
|
||||
class TypingLatencyReportDialog(
|
||||
private val project: Project,
|
||||
private val message: String = "",
|
||||
private val threadDumps: List<String> = emptyList()
|
||||
) : DialogWrapper(project) {
|
||||
private var currentThreadDump = 0
|
||||
@@ -51,16 +50,12 @@ class TypingLatencyReportDialog(
|
||||
|
||||
override fun createCenterPanel(): JComponent {
|
||||
val jbScrollPane = createReportTree()
|
||||
val topPane: JComponent = if (message.isNotEmpty())
|
||||
JBUI.Panels.simplePanel().addToTop(JLabel(message)).addToCenter(jbScrollPane)
|
||||
else
|
||||
jbScrollPane
|
||||
|
||||
if (threadDumps.isEmpty()) {
|
||||
return topPane
|
||||
return jbScrollPane
|
||||
}
|
||||
return JBSplitter(true).apply {
|
||||
firstComponent = topPane
|
||||
firstComponent = jbScrollPane
|
||||
secondComponent = createThreadDumpBrowser()
|
||||
}
|
||||
}
|
||||
@@ -87,7 +82,7 @@ class TypingLatencyReportDialog(
|
||||
if (value == null) return
|
||||
val obj = (value as DefaultMutableTreeNode).userObject
|
||||
if (obj is LatencyDistributionRecord) {
|
||||
append(formatLatency(obj.key.name, obj.totalLatency))
|
||||
append(formatLatency(obj.key.name, obj.totalLatency, obj.key.details))
|
||||
}
|
||||
else if (obj is Pair<*, *>) {
|
||||
val pair = obj as Pair<String, LatencyRecord>
|
||||
@@ -100,8 +95,12 @@ class TypingLatencyReportDialog(
|
||||
return JBScrollPane(reportList)
|
||||
}
|
||||
|
||||
private fun formatLatency(action: String, latencyRecord: LatencyRecord): String {
|
||||
return "$action - avg ${latencyRecord.averageLatency} ms, max ${latencyRecord.maxLatency} ms"
|
||||
private fun formatLatency(action: String, latencyRecord: LatencyRecord, details: String? = null): String {
|
||||
val result = "$action - avg ${latencyRecord.averageLatency} ms, max ${latencyRecord.maxLatency} ms"
|
||||
if (details != null) {
|
||||
return "$result, $details"
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun createThreadDumpBrowser(): JComponent {
|
||||
@@ -142,22 +141,19 @@ class TypingLatencyReportDialog(
|
||||
|
||||
private fun formatReportAsText(): String {
|
||||
return buildString {
|
||||
if (message.isNotEmpty()) {
|
||||
append(message)
|
||||
}
|
||||
for (row in latencyMap.values.sortedBy { it.key.name }) {
|
||||
appendln(formatLatency(row.key.name, row.totalLatency))
|
||||
appendln(formatLatency(row.key.name, row.totalLatency, row.key.details))
|
||||
appendln("Actions:")
|
||||
for (actionLatencyRecord in row.actionLatencyRecords.entries.sortedByDescending { it.value.averageLatency }) {
|
||||
appendln(" ${formatLatency(actionLatencyRecord.key, actionLatencyRecord.value)}")
|
||||
}
|
||||
appendln()
|
||||
if (threadDumps.isNotEmpty()) {
|
||||
appendln("Thread dumps:")
|
||||
for (threadDump in threadDumps) {
|
||||
appendln(threadDump)
|
||||
appendln("-".repeat(40))
|
||||
}
|
||||
}
|
||||
appendln()
|
||||
if (threadDumps.isNotEmpty()) {
|
||||
appendln("Thread dumps:")
|
||||
for (threadDump in threadDumps) {
|
||||
appendln(threadDump)
|
||||
appendln("-".repeat(40))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,16 +95,17 @@ interface RetypeFileAssistant {
|
||||
}
|
||||
}
|
||||
|
||||
class RetypeQueue(private val project: Project, private val retypeDelay: Int, private val threadDumpDelay: Int) {
|
||||
private class RetypeQueue(private val project: Project, private val retypeDelay: Int, private val threadDumpDelay: Int) {
|
||||
val files = mutableListOf<VirtualFile>()
|
||||
private val threadDumps = mutableListOf<String>()
|
||||
|
||||
fun processNext() {
|
||||
if (files.isEmpty()) return
|
||||
val file = files[0]
|
||||
files.removeAt(0)
|
||||
|
||||
val editor = FileEditorManager.getInstance(project).openTextEditor(OpenFileDescriptor(project, file), true)
|
||||
val retypeSession = RetypeSession(project, editor as EditorImpl, retypeDelay, threadDumpDelay)
|
||||
val editor = FileEditorManager.getInstance(project).openTextEditor(OpenFileDescriptor(project, file, 0), true)
|
||||
val retypeSession = RetypeSession(project, editor as EditorImpl, retypeDelay, threadDumpDelay, threadDumps)
|
||||
if (files.isNotEmpty()) {
|
||||
retypeSession.startNextCallback = {
|
||||
ApplicationManager.getApplication().invokeLater { processNext() }
|
||||
|
||||
@@ -6,6 +6,7 @@ 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.TemplateManager
|
||||
import com.intellij.codeInsight.template.impl.LiveTemplateLookupElement
|
||||
import com.intellij.diagnostic.ThreadDumper
|
||||
import com.intellij.ide.DataManager
|
||||
@@ -16,6 +17,7 @@ import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.actionSystem.AnActionEvent
|
||||
import com.intellij.openapi.actionSystem.IdeActions
|
||||
import com.intellij.openapi.actionSystem.ex.ActionManagerEx
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.command.WriteCommandAction
|
||||
import com.intellij.openapi.diagnostic.Logger
|
||||
import com.intellij.openapi.editor.LogicalPosition
|
||||
@@ -33,7 +35,8 @@ class RetypeSession(
|
||||
private val project: Project,
|
||||
private val editor: EditorImpl,
|
||||
private val delayMillis: Int,
|
||||
private val threadDumpDelay: Int
|
||||
private val threadDumpDelay: Int,
|
||||
private val threadDumps: MutableList<String> = mutableListOf()
|
||||
) : Disposable {
|
||||
private val document = editor.document
|
||||
private val alarm = Alarm(Alarm.ThreadToUse.SWING_THREAD, this)
|
||||
@@ -45,11 +48,12 @@ class RetypeSession(
|
||||
private var typedChars = 0
|
||||
private var completedChars = 0
|
||||
private var backtrackedChars = 0
|
||||
private val threadDumps = mutableListOf<String>()
|
||||
private val oldSelectAutopopup = CodeInsightSettings.getInstance().SELECT_AUTOPOPUP_SUGGESTIONS_BY_CHARS
|
||||
private val oldAddUnambiguous = CodeInsightSettings.getInstance().ADD_UNAMBIGIOUS_IMPORTS_ON_THE_FLY
|
||||
private var needSyncPosition = false
|
||||
private var editorLineBeforeAcceptingLookup = -1
|
||||
var startNextCallback: (() -> Unit)? = null
|
||||
private val disposeLock = Any()
|
||||
|
||||
val currentLineText get() = lines[line]
|
||||
|
||||
@@ -58,16 +62,29 @@ class RetypeSession(
|
||||
val vFile = FileDocumentManager.getInstance().getFile(document)
|
||||
val keyName = "${vFile?.name ?: "Unknown file"} (${document.textLength} chars)"
|
||||
currentLatencyRecordKey = LatencyDistributionRecordKey(keyName)
|
||||
WriteCommandAction.runWriteCommandAction(project) { document.deleteString(0, document.textLength) }
|
||||
CodeInsightSettings.getInstance().SELECT_AUTOPOPUP_SUGGESTIONS_BY_CHARS = false
|
||||
line = editor.caretModel.logicalPosition.line - 1
|
||||
val currentLineStart = document.getLineStartOffset(line + 1)
|
||||
WriteCommandAction.runWriteCommandAction(project) { document.deleteString(currentLineStart, document.textLength) }
|
||||
CodeInsightSettings.getInstance().apply {
|
||||
SELECT_AUTOPOPUP_SUGGESTIONS_BY_CHARS = false
|
||||
ADD_UNAMBIGIOUS_IMPORTS_ON_THE_FLY = false
|
||||
}
|
||||
queueNext()
|
||||
}
|
||||
|
||||
fun stop(startNext: Boolean) {
|
||||
WriteCommandAction.runWriteCommandAction(project) { document.replaceString(0, document.textLength, originalText) }
|
||||
Disposer.dispose(this)
|
||||
if (!ApplicationManager.getApplication().isUnitTestMode) {
|
||||
WriteCommandAction.runWriteCommandAction(project) { document.replaceString(0, document.textLength, originalText) }
|
||||
}
|
||||
synchronized(disposeLock) {
|
||||
Disposer.dispose(this)
|
||||
}
|
||||
editor.putUserData(RETYPE_SESSION_KEY, null)
|
||||
CodeInsightSettings.getInstance().SELECT_AUTOPOPUP_SUGGESTIONS_BY_CHARS = oldSelectAutopopup
|
||||
CodeInsightSettings.getInstance().apply {
|
||||
SELECT_AUTOPOPUP_SUGGESTIONS_BY_CHARS = oldSelectAutopopup
|
||||
ADD_UNAMBIGIOUS_IMPORTS_ON_THE_FLY = oldAddUnambiguous
|
||||
}
|
||||
currentLatencyRecordKey?.details = "typed $typedChars chars, completed $completedChars chars, backtracked $backtrackedChars chars"
|
||||
currentLatencyRecordKey = null
|
||||
if (startNext) {
|
||||
startNextCallback?.invoke()
|
||||
@@ -104,83 +121,88 @@ class RetypeSession(
|
||||
}
|
||||
syncPositionWithEditor()
|
||||
}
|
||||
needSyncPosition = false
|
||||
|
||||
if (TemplateManager.getInstance(project).getActiveTemplate(editor) != null) {
|
||||
TemplateManager.getInstance(project).finishTemplate(editor)
|
||||
queueNextOrStop(true)
|
||||
return
|
||||
}
|
||||
|
||||
val lookup = LookupManager.getActiveLookup(editor) as LookupImpl?
|
||||
var lookupSelected = false
|
||||
if (lookup != null) {
|
||||
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) {
|
||||
val currentLookupElement = lookup.currentItem
|
||||
if (currentLookupElement?.shouldAccept(lookup.lookupStart) == true) {
|
||||
lookup.focusDegree = LookupImpl.FocusDegree.FOCUSED
|
||||
editorLineBeforeAcceptingLookup = editor.caretModel.logicalPosition.line
|
||||
executeEditorAction(IdeActions.ACTION_CHOOSE_LOOKUP_ITEM)
|
||||
needSyncPosition = true
|
||||
lookupSelected = true
|
||||
queueNextOrStop(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (!lookupSelected) {
|
||||
val c = currentLineText[column]
|
||||
typedChars++
|
||||
if (c == '\n') {
|
||||
column = 0 // line will be incremented in next loop
|
||||
val c = currentLineText[column]
|
||||
typedChars++
|
||||
if (c == '\n') {
|
||||
column = 0 // line will be incremented in next loop
|
||||
|
||||
// Check if the next line was partially inserted with some insert handler (e.g. braces in java)
|
||||
if (line + 1 < document.lineCount
|
||||
&& line + 1 < lines.size
|
||||
&& lines[line + 1].startsWith(getEditorLineText(line + 1))) {
|
||||
// the caret will be moved right during the next position sync
|
||||
executeEditorAction(IdeActions.ACTION_EDITOR_MOVE_CARET_RIGHT)
|
||||
}
|
||||
else {
|
||||
executeEditorAction(IdeActions.ACTION_EDITOR_ENTER)
|
||||
}
|
||||
// Check if the next line was partially inserted with some insert handler (e.g. braces in java)
|
||||
if (line + 1 < document.lineCount
|
||||
&& line + 1 < lines.size
|
||||
&& lines[line + 1].startsWith(getEditorLineText(line + 1))) {
|
||||
// the caret will be moved right during the next position sync
|
||||
executeEditorAction(IdeActions.ACTION_EDITOR_MOVE_CARET_RIGHT)
|
||||
}
|
||||
else {
|
||||
column++
|
||||
editor.type(c.toString())
|
||||
executeEditorAction(IdeActions.ACTION_EDITOR_ENTER)
|
||||
}
|
||||
}
|
||||
else {
|
||||
column++
|
||||
editor.type(c.toString())
|
||||
}
|
||||
queueNextOrStop(false)
|
||||
}
|
||||
|
||||
private fun queueNextOrStop(needSyncPosition: Boolean) {
|
||||
this.needSyncPosition = needSyncPosition
|
||||
if ((column == 0 && line < lines.size - 1) || (column > 0 && column < currentLineText.length)) {
|
||||
queueNext()
|
||||
}
|
||||
else {
|
||||
stop(true)
|
||||
|
||||
val message = buildString {
|
||||
val file = FileDocumentManager.getInstance().getFile(document)
|
||||
if (file != null) {
|
||||
append(file.name)
|
||||
append(" ")
|
||||
}
|
||||
append("Typed $typedChars chars, completed $completedChars chars, backtracked $backtrackedChars chars")
|
||||
}
|
||||
|
||||
if (startNextCallback == null) {
|
||||
TypingLatencyReportDialog(project, message, threadDumps).show()
|
||||
if (startNextCallback == null && !ApplicationManager.getApplication().isUnitTestMode) {
|
||||
TypingLatencyReportDialog(project, threadDumps).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isLookupElementAcceptable(lookupElement: LookupElement?): Boolean {
|
||||
if (lookupElement == null) return false
|
||||
private fun LookupElement.shouldAccept(lookupStartOffset: Int): Boolean {
|
||||
for (retypeFileAssistant in Extensions.getExtensions(
|
||||
RetypeFileAssistant.EP_NAME)) {
|
||||
if (!retypeFileAssistant.acceptLookupElement(lookupElement)) {
|
||||
if (!retypeFileAssistant.acceptLookupElement(this)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return lookupElement !is LiveTemplateLookupElement
|
||||
if (this is LiveTemplateLookupElement) {
|
||||
return false
|
||||
}
|
||||
val lookupString = LookupElementPresentation.renderElement(this).itemText ?: return false
|
||||
val lookupStartColumn = editor.offsetToLogicalPosition(lookupStartOffset).column
|
||||
val textAtColumn = currentLineText.drop(lookupStartColumn)
|
||||
if (textAtColumn.take(lookupString.length) != lookupString) {
|
||||
return false
|
||||
}
|
||||
return textAtColumn.length == lookupString.length ||
|
||||
!Character.isJavaIdentifierPart(textAtColumn[lookupString.length] + 1)
|
||||
}
|
||||
|
||||
private fun checkPrevLineInSync(): Boolean {
|
||||
val prevLine = getEditorLineText(editor.caretModel.logicalPosition.line - 1)
|
||||
if (prevLine.trimEnd() != currentLineText.trimEnd()) {
|
||||
stop(false)
|
||||
Messages.showErrorDialog(project, "Text has diverged. Expected:\n$currentLineText\nActual:\n$prevLine",
|
||||
"Retype File")
|
||||
stop(false)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -233,7 +255,11 @@ class RetypeSession(
|
||||
private fun logThreadDump() {
|
||||
if (editor.isProcessingTypedAction) {
|
||||
threadDumps.add(ThreadDumper.dumpThreadsToString())
|
||||
threadDumpAlarm.addRequest({ logThreadDump() }, 100)
|
||||
synchronized(disposeLock) {
|
||||
if (!threadDumpAlarm.isDisposed) {
|
||||
threadDumpAlarm.addRequest({ logThreadDump() }, 100)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,9 @@ class LatencyRecord {
|
||||
val averageLatency: Long get() = totalLatency / totalKeysTyped
|
||||
}
|
||||
|
||||
data class LatencyDistributionRecordKey(val name: String)
|
||||
data class LatencyDistributionRecordKey(val name: String) {
|
||||
var details: String? = null
|
||||
}
|
||||
|
||||
class LatencyDistributionRecord(val key: LatencyDistributionRecordKey) {
|
||||
val totalLatency: LatencyRecord = LatencyRecord()
|
||||
|
||||
Reference in New Issue
Block a user