Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,22 @@ import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.idetooltips.TooltipTag
import com.itsaky.androidide.lsp.kotlin.KotlinLanguageServer
import com.itsaky.androidide.lsp.kotlin.compiler.modules.ScheduledCancelChecker
import com.itsaky.androidide.lsp.kotlin.refactor.ui.ExtractMethodChoice
import com.itsaky.androidide.lsp.kotlin.refactor.ui.ExtractMethodSheet
import com.itsaky.androidide.lsp.kotlin.refactor.KOTLIN_NAME_MESSAGES
import com.itsaky.androidide.lsp.kotlin.refactor.candidateFor
import com.itsaky.androidide.lsp.kotlin.refactor.toMethodCandidateViews
import com.itsaky.androidide.lsp.kotlin.utils.refactor.ExtractMethodCandidate
import com.itsaky.androidide.lsp.kotlin.utils.refactor.ExtractMethodPlan
import com.itsaky.androidide.lsp.kotlin.utils.refactor.ExtractionRefusal
import com.itsaky.androidide.lsp.kotlin.utils.refactor.HARD_KEYWORDS
import com.itsaky.androidide.lsp.kotlin.utils.refactor.buildExtractMethodPlan
import com.itsaky.androidide.lsp.kotlin.utils.refactor.buildExtractMethodRewrites
import com.itsaky.androidide.lsp.models.CodeActionItem
import com.itsaky.androidide.lsp.models.CodeActionKind
import com.itsaky.androidide.lsp.models.Command
import com.itsaky.androidide.lsp.models.DocumentChange
import com.itsaky.androidide.lsp.refactor.toTextEdit
import com.itsaky.androidide.lsp.ui.ExtractMethodSelection
import com.itsaky.androidide.lsp.ui.ExtractMethodSheet
import com.itsaky.androidide.lsp.ui.findFragmentActivity
import com.itsaky.androidide.projects.FileManager
import com.itsaky.androidide.resources.R
Expand Down Expand Up @@ -99,7 +104,13 @@ class ExtractMethodAction : BaseKotlinCodeAction() {
return
}

val shown = ExtractMethodSheet.show(activity, result) { choice -> applyChoice(data, result, choice) }
val shown =
ExtractMethodSheet.show(
activity,
result.toMethodCandidateViews(),
HARD_KEYWORDS,
KOTLIN_NAME_MESSAGES,
) { selection -> applySelection(data, result, selection) }
if (!shown) {
logger.warn("Fragment manager unavailable. Cannot show the extract sheet.")
}
Expand All @@ -115,21 +126,21 @@ class ExtractMethodAction : BaseKotlinCodeAction() {
* Runs from the sheet's click handler, outside `execAction` and so outside every guard the action
* framework provides -- nothing here may throw (R16), hence the [runCatching].
*/
private fun applyChoice(
private fun applySelection(
data: ActionData,
plan: ExtractMethodPlan,
choice: ExtractMethodChoice,
selection: ExtractMethodSelection,
) {
runCatching { performChoice(data, plan, choice) }.onFailure { error ->
logger.error("Failed to apply the extract-method choice '{}'", choice.name, error)
runCatching { performSelection(data, plan, selection) }.onFailure { error ->
logger.error("Failed to apply the extract-method selection '{}'", selection.name, error)
flashError(R.string.msg_cannot_perform_fix)
}
}

private fun performChoice(
private fun performSelection(
data: ActionData,
plan: ExtractMethodPlan,
choice: ExtractMethodChoice,
selection: ExtractMethodSelection,
) {
val file = data.requireFile()
val nioPath = file.toPath()
Expand All @@ -138,9 +149,16 @@ class ExtractMethodAction : BaseKotlinCodeAction() {
return
}

val candidate: ExtractMethodCandidate =
plan.candidateFor(selection) ?: run {
logger.warn("Selection {} does not address the plan it came from.", selection)
flashError(R.string.msg_cannot_perform_fix)
return
}

val rewrites =
buildExtractMethodRewrites(plan.fileText, choice.candidate, choice.name) ?: run {
logger.warn("Could not build an extract-method rewrite for '{}'", choice.candidate.label)
buildExtractMethodRewrites(plan.fileText, candidate, selection.name) ?: run {
logger.warn("Could not build an extract-method rewrite for '{}'", candidate.label)
flashError(R.string.msg_cannot_perform_fix)
return
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.itsaky.androidide.lsp.kotlin.refactor

import com.itsaky.androidide.lsp.kotlin.utils.refactor.ExtractMethodCandidate
import com.itsaky.androidide.lsp.kotlin.utils.refactor.ExtractMethodPlan
import com.itsaky.androidide.lsp.kotlin.utils.refactor.signaturePrefix
import com.itsaky.androidide.lsp.kotlin.utils.refactor.signatureSuffix
import com.itsaky.androidide.lsp.ui.ExtractMethodSelection
import com.itsaky.androidide.lsp.ui.MethodCandidateView

/**
* The plan as the shared sheet sees it: labels, names and the two halves of the signature, no PSI and
* no offsets.
*
* Offsets stay on this side deliberately -- the sheet is a chooser, and resolving a selection back into
* a candidate is [candidateFor]'s job.
*/
fun ExtractMethodPlan.toMethodCandidateViews(): List<MethodCandidateView> =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Daniel-ADFA low - the adapter mapping is now untested.

Neither toMethodCandidateViews() nor candidateFor() has a test, and the deleted :lsp:kotlin ExtractMethodViewModelTest was the only place a real ExtractMethodCandidate was rendered through the preview path. Its :lsp:ui replacement asserts on hardcoded prefix/suffix literals, so nothing pins that this function fills signaturePrefix/signatureSuffix from the candidate at all.

Concretely: swap lines 23 and 24 and the entire suite still passes - ExtractMethodEditTest:406 only covers the composed signatureText, which is symmetric under the swap. A three-line test over toMethodCandidateViews() + candidateFor() restores what the move dropped.

candidates.map { candidate ->
MethodCandidateView(
label = candidate.label,
suggestedName = candidate.suggestedName,
takenNames = candidate.takenNames,
signaturePrefix = candidate.signaturePrefix,
signatureSuffix = candidate.signatureSuffix,
)
}

/**
* Resolves a selection's index back to the plan it came from, or null when it does not address it.
*
* A null is a wiring bug rather than a user path -- the sheet only ever reports an index it was given --
* so the caller reports it as a failed quick fix rather than guessing at a candidate.
*/
fun ExtractMethodPlan.candidateFor(selection: ExtractMethodSelection): ExtractMethodCandidate? =
candidates.getOrNull(selection.candidateIndex)

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -183,18 +183,31 @@ data class ExtractMethodPlan(
}

/**
* The signature exactly as [buildExtractMethodRewrites] emits it. The sheet's preview calls this, so
* there is one derivation and the preview cannot drift from the declaration (R11).
* Everything the signature says before the method's name.
*
* Split from [signatureSuffix] rather than rendered whole because the sheet's preview follows what the
* user types, and [MethodCandidateView] carries the two halves. Both this and

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Daniel-ADFA low - unresolvable KDoc link.

[MethodCandidateView] lives in com.itsaky.androidide.lsp.ui and is not imported in this file, so the reference resolves in neither the IDE nor Dokka - it renders as literal text. Use the fully-qualified name in the link, or drop it to plain code formatting.

* [buildExtractMethodRewrites] compose them through [signatureText], so there is one derivation and
* the preview cannot drift from the declaration (R11).
*/
fun ExtractMethodCandidate.signatureText(name: String): String =
buildString {
annotations.forEach { append(it).append(' ') }
modifiers.forEach { append(it).append(' ') }
append("fun ")
receiverTypeText?.let { append(it).append('.') }
append(name)
append('(')
append(parameters.joinToString(", ") { "${it.name}: ${it.typeText}" })
append(')')
returnTypeText?.let { append(": ").append(it) }
}
val ExtractMethodCandidate.signaturePrefix: String
get() =
buildString {
annotations.forEach { append(it).append(' ') }
modifiers.forEach { append(it).append(' ') }
append("fun ")
receiverTypeText?.let { append(it).append('.') }
}

/** Everything the signature says after the method's name: parameters and return type. */
val ExtractMethodCandidate.signatureSuffix: String
get() =
buildString {
append('(')
append(parameters.joinToString(", ") { "${it.name}: ${it.typeText}" })
append(')')
returnTypeText?.let { append(": ").append(it) }
}

/** The signature exactly as [buildExtractMethodRewrites] emits it. */
fun ExtractMethodCandidate.signatureText(name: String): String = signaturePrefix + name + signatureSuffix
Loading
Loading