Retype: better results presentation, fixed a couple more desync cases, add failing test

This commit is contained in:
Dmitry Jemerov
2018-07-20 18:55:57 +02:00
parent b837540690
commit a36c018c11
7 changed files with 150 additions and 74 deletions
@@ -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)
}
}
@@ -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()