Skip to content

Commit

Permalink
Revert "refactor: clean inlay model"
Browse files Browse the repository at this point in the history
This reverts commit e5d0905.
  • Loading branch information
jialiu-github committed Mar 19, 2024
1 parent 8d66903 commit 93aa5a8
Show file tree
Hide file tree
Showing 15 changed files with 1,099 additions and 7 deletions.
25 changes: 24 additions & 1 deletion src/222/main/resources/META-INF/autodev-core.xml
Expand Up @@ -26,6 +26,9 @@
parentId="cc.unitmesh.devti.settings.AutoDevSettingsConfigurable"
id="cc.unitmesh.autodevCoder"
bundle="messages.AutoDevBundle" key="settings.autodev.coder"/>
<applicationService
serviceInterface="cc.unitmesh.devti.editor.inlay.LLMInlayManager"
serviceImplementation="cc.unitmesh.devti.editor.inlay.LLMInlayManagerImpl"/>

<applicationService serviceImplementation="cc.unitmesh.devti.settings.AutoDevSettingsState"/>

Expand Down Expand Up @@ -59,6 +62,9 @@

<notificationGroup id="AI notification group" displayType="STICKY_BALLOON" bundle="messages.AutoDevBundle"
key="name"/>
<!-- <editorFactoryListener implementation="cc.unitmesh.devti.editor.inlay.AutoDevEditorListener"/>-->
<!-- <typedHandler order="first, before completionAutoPopup"-->
<!-- implementation="cc.unitmesh.devti.editor.inlay.TypeOverHandler"/>-->

<intentionAction>
<className>cc.unitmesh.devti.intentions.AutoDevIntentionHelper</className>
Expand Down Expand Up @@ -160,6 +166,10 @@
<listener topic="com.intellij.ide.plugins.DynamicPluginListener"
class="cc.unitmesh.devti.AutoDevUnloadListener"/>
</applicationListeners>
<!-- <projectListeners>-->
<!-- <listener topic="com.intellij.openapi.command.CommandListener"-->
<!-- class="cc.unitmesh.devti.editor.inlay.LLMCommandListener"/>-->
<!-- </projectListeners>-->

<extensions defaultExtensionNs="cc.unitmesh">
<autoDevIntention>
Expand All @@ -173,7 +183,13 @@
<categoryKey>intention.category.llm</categoryKey>
</autoDevIntention>
<autoDevIntention>
<className>cc.unitmesh.devti.intentions.action.AutoTestThisBaseIntention</className>
<!-- <className>cc.unitmesh.devti.intentions.action.AutoTestThisBaseIntention</className>-->
<className>cc.unitmesh.devti.intentions.action.CodeCompletionInlayIntention</className>
<bundleName>messages.AutoDevBundle</bundleName>
<categoryKey>intention.category.llm</categoryKey>
</autoDevIntention>
<autoDevIntention>
<className>cc.unitmesh.devti.intentions.action.AutoTestThisIntention</className>
<bundleName>messages.AutoDevBundle</bundleName>
<categoryKey>intention.category.llm</categoryKey>
</autoDevIntention>
Expand All @@ -187,6 +203,13 @@
</extensions>

<actions>
<action id="llm.applyInlays"
class="cc.unitmesh.devti.actions.LLMApplyInlaysAction">
<keyboard-shortcut first-keystroke="TAB" keymap="$default"/>
<override-text place="MainMenu" text="Apply Completions to Editor"/>
<override-text place="EditorPopup" text="Accept"/>
</action>

<group id="AutoDevIntentionsActionGroup" class="cc.unitmesh.devti.intentions.IntentionsActionGroup"
icon="cc.unitmesh.devti.AutoDevIcons.AI_COPILOT" searchable="false">

Expand Down
119 changes: 119 additions & 0 deletions src/main/kotlin/cc/unitmesh/devti/actions/LLMApplyInlaysAction.kt
@@ -0,0 +1,119 @@
package cc.unitmesh.devti.actions

import cc.unitmesh.devti.editor.inlay.LLMInlayManager
import com.intellij.temporary.inlay.presentation.EditorUtilCopy
import com.intellij.application.options.CodeStyle
import com.intellij.codeInsight.lookup.LookupManager
import com.intellij.codeInsight.template.TemplateManager
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.EditorAction
import com.intellij.openapi.editor.actionSystem.EditorActionHandler
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.util.TextRange
import java.awt.event.KeyEvent

class LLMApplyInlaysAction : EditorAction(ApplyInlaysHandler()), DumbAware {
init {
setInjectedContext(true)
}

override fun update(e: AnActionEvent) {
if (isIgnoredKeyboardEvent(e)) {
e.presentation.isEnabled = false
return
}

super.update(e)
}

private fun isIgnoredKeyboardEvent(e: AnActionEvent): Boolean {
if (e.inputEvent !is KeyEvent) return false
if ((e.inputEvent as KeyEvent).keyChar != '\t') return false

val project = e.project ?: return false
val editor = getEditor(e.dataContext) ?: return false

val document = editor.document
val blockIndent = CodeStyle.getIndentOptions(project, document).INDENT_SIZE
val caretOffset = editor.caretModel.offset
val line = document.getLineNumber(caretOffset)

val caretOffsetAfterTab = EditorUtilCopy.indentLine(project, editor, line, blockIndent, caretOffset)
// if (isNonEmptyLinePrefix(document, line, caretOffset) || caretOffsetAfterTab < caretOffset) {
// return false
// }

val instance = LLMInlayManager.getInstance()
val tabRange = TextRange.create(caretOffset, caretOffsetAfterTab)

if (instance.countCompletionInlays(editor, tabRange) > 0) {
return false
}

return true
}


private class ApplyInlaysHandler : EditorActionHandler() {
override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext): Boolean {
return isSupported(editor)
}

override fun executeInCommand(editor: Editor, dataContext: DataContext): Boolean {
return false
}

override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext) {
if (editor.isDisposed) return
val project = editor.project ?: return
if (project.isDisposed) return


// todo: update this to use the new API
logger.info("doExecute for applyInlays")
LLMInlayManager.getInstance().applyCompletion(project, editor)
}
}

companion object {
const val ID = "llm.applyInlays"

val logger = logger<LLMApplyInlaysAction>()

private fun isSpaceOrTab(c: Char, withNewline: Boolean): Boolean {
return c == ' ' || c == '\t' || withNewline && c == '\n'
}

private fun isSpacesOrTabs(text: CharSequence, withNewlines: Boolean): Boolean {
for (element in text) {
if (!isSpaceOrTab(element, withNewlines)) {
return false
}
}
return true
}

private fun isNonEmptyLinePrefix(document: Document, lineNumber: Int, caretOffset: Int): Boolean {
val lineStartOffset = document.getLineStartOffset(lineNumber)
if (lineStartOffset == caretOffset) {
return false
}
val linePrefix = document.getText(TextRange.create(lineStartOffset, caretOffset))
return !isSpacesOrTabs(linePrefix, false)
}

fun isSupported(editor: Editor): Boolean {
val project = editor.project
return project != null && editor
.caretModel.caretCount == 1 && (LookupManager.getActiveLookup(editor) == null) && TemplateManager.getInstance(
project
)
.getActiveTemplate(editor) == null
}
}
}
@@ -0,0 +1,60 @@
package cc.unitmesh.devti.editor.inlay

import com.intellij.openapi.command.CommandProcessor
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.event.*
import com.intellij.openapi.editor.ex.util.EditorUtil
import com.intellij.openapi.util.Disposer

class AutoDevEditorListener : EditorFactoryListener {
override fun editorCreated(event: EditorFactoryEvent) {
val editor = event.editor
val project = editor.project ?: return
if (project.isDisposed) return

val editorDisposable = Disposer.newDisposable("autoDevEditorListener")
EditorUtil.disposeWithEditor(editor, editorDisposable)

editor.document.addDocumentListener(AutoDevDocumentListener(editor), editorDisposable)
editor.caretModel.addCaretListener(AutoDevCaretListener(editor), editorDisposable)
}

class AutoDevCaretListener(val editor: Editor) : CaretListener {
override fun caretPositionChanged(event: CaretEvent) {
val project = editor.project
if (project == null || project.isDisposed) return

val llmInlayManager = LLMInlayManager.getInstance()

val wasTypeOver = TypeOverHandler.getPendingTypeOverAndReset(editor)
if (wasTypeOver) {
llmInlayManager.editorModified(editor)
return
}

if (CommandProcessor.getInstance().currentCommand != null) {
return
}

llmInlayManager.disposeInlays(editor, InlayDisposeContext.CaretChange)
}
}

class AutoDevDocumentListener(val editor: Editor) : BulkAwareDocumentListener {
override fun documentChangedNonBulk(event: DocumentEvent) {
val project = editor.project
if (project == null || project.isDisposed) return

val commandProcessor = CommandProcessor.getInstance()
if (commandProcessor.isUndoTransparentActionInProgress) return
if (commandProcessor.currentCommandName != null) return

val changeOffset = event.offset + event.newLength
if (editor.caretModel.offset != changeOffset) return

val llmInlayManager = LLMInlayManager.getInstance()
llmInlayManager
.editorModified(editor, changeOffset)
}
}
}
@@ -0,0 +1,19 @@
package cc.unitmesh.devti.editor.inlay

enum class InlayDisposeContext {
UserAction,
IdeCompletion,
CaretChange,
SelectionChange,
SettingsChange,
Cycling,
TypingAsSuggested,
Typing,
Applied,
;

val isResetLastRequest: Boolean
get() = this == SettingsChange || this == Applied
val isSendRejectedTelemetry: Boolean
get() = this == UserAction
}
115 changes: 115 additions & 0 deletions src/main/kotlin/cc/unitmesh/devti/editor/inlay/LLMCommandListener.kt
@@ -0,0 +1,115 @@
package cc.unitmesh.devti.editor.inlay

import com.intellij.openapi.command.CommandEvent
import com.intellij.openapi.command.CommandListener
import com.intellij.openapi.diagnostic.logger
import com.intellij.openapi.editor.Document
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.VisualPosition
import com.intellij.openapi.editor.ex.DocumentEx
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Key
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicReference

private class UndoTransparentActionState(val editor: Editor, val modificationStamp: Long)
private class CommandEditorState(val modificationStamp: Long, val visualPosition: VisualPosition)

class LLMCommandListener(private val project: Project) : CommandListener {
private val activeCommands = AtomicInteger()
private val startedWithEditor = AtomicBoolean(false)
private val undoTransparentActionStamp = AtomicReference<UndoTransparentActionState?>()

override fun commandStarted(event: CommandEvent) {
if (activeCommands.getAndIncrement() > 0) {
logger.debug("Skipping nested commandStarted. Event: $event")
return
}

val editor = getSelectedEditorSafely(project)
if (editor != null) {
startedWithEditor.set(true)
COMMAND_STATE_KEY[editor] = createCommandState(editor)
} else {
startedWithEditor.set(false)
}
}


override fun commandFinished(event: CommandEvent) {
if (activeCommands.decrementAndGet() > 0) {
logger.debug("Skipping nested commandFinished. Event: $event")
return
}

if (!startedWithEditor.get()) return

val editor = getSelectedEditorSafely(project) ?: return
val editorManager = LLMInlayManager.getInstance()

if (!editorManager.isAvailable(editor)) return

val commandStartState = COMMAND_STATE_KEY[editor] ?: return
val commandEndState = createCommandState(editor)
if (isDocumentModification(commandStartState, commandEndState)) {
logger.debug("command modified document: " + event.commandName)
editorManager.editorModified(editor)
} else if (isCaretPositionChange(commandStartState, commandEndState)) {
editorManager.disposeInlays(editor, InlayDisposeContext.CaretChange)
}
}

override fun undoTransparentActionStarted() {
val editor = getSelectedEditorSafely(project)
undoTransparentActionStamp.set(if (editor != null) createUndoTransparentState(editor) else null)
}

override fun undoTransparentActionFinished() {
val currentEditorStamp = undoTransparentActionStamp.get()
undoTransparentActionStamp.set(null)
val editor = getSelectedEditorSafely(project) ?: return

if (currentEditorStamp == null || editor !== currentEditorStamp.editor) return
if (getDocumentStamp(editor.document) == currentEditorStamp.modificationStamp) return

val editorManager = LLMInlayManager.getInstance()
if (editorManager.isAvailable(editor)) {
editorManager.editorModified(editor)
}
}

private fun getSelectedEditorSafely(project: Project): Editor? {
return try {
FileEditorManager.getInstance(project)?.selectedTextEditor
} catch (e: Exception) {
null
}
}

companion object {
private val logger = logger<LLMCommandListener>()
private val COMMAND_STATE_KEY = Key.create<CommandEditorState>("llm.commandState")
}

private fun createCommandState(editor: Editor): CommandEditorState {
return CommandEditorState(getDocumentStamp(editor.document), editor.caretModel.visualPosition)
}

private fun createUndoTransparentState(editor: Editor): UndoTransparentActionState {
return UndoTransparentActionState(editor, getDocumentStamp(editor.document))
}

private fun getDocumentStamp(document: Document): Long {
return if (document is DocumentEx) document.modificationSequence.toLong() else document.modificationStamp
}

private fun isDocumentModification(first: CommandEditorState, second: CommandEditorState): Boolean {
return first.modificationStamp != second.modificationStamp
}

private fun isCaretPositionChange(first: CommandEditorState, second: CommandEditorState): Boolean {
return first.visualPosition != second.visualPosition
}
}
@@ -0,0 +1,11 @@
package cc.unitmesh.devti.editor.inlay

import com.intellij.openapi.editor.Document
import com.intellij.openapi.fileEditor.FileDocumentSynchronizationVetoer
import com.intellij.openapi.util.UserDataHolder

class LLMEditorSaveVetoer : FileDocumentSynchronizationVetoer() {
override fun maySaveDocument(document: Document, isSaveExplicit: Boolean): Boolean {
return !LLMInlayManagerImpl.KEY_DOCUMENT_SAVE_VETO.isIn(document as UserDataHolder)
}
}

0 comments on commit 93aa5a8

Please sign in to comment.