Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
70 commits
Select commit Hold shift + click to select a range
21a4dfd
ADFA-4826: Add shared IDE Compose theming in common-compose
itsaky-adfa Aug 10, 2026
46cf261
ADFA-4826: Move profiler and floating-window onto the shared theming
itsaky-adfa Aug 10, 2026
97470d3
ADFA-4826: Enable Compose in lsp/kotlin
itsaky-adfa Aug 10, 2026
8442302
ADFA-4826: Add extract-variable analysis, plan and rewrite
itsaky-adfa Aug 10, 2026
196e3e1
ADFA-4826: Add the extract-variable Compose sheet
itsaky-adfa Aug 10, 2026
22c7b4c
ADFA-4826: Wire up the extract-variable code action
itsaky-adfa Aug 10, 2026
e72f138
ADFA-4826: Document the extract-variable requirements
itsaky-adfa Aug 10, 2026
6956963
ADFA-4826: Stop offering the lambda that wraps the expression
itsaky-adfa Aug 12, 2026
756d269
ADFA-4826: Label a block rung by the construct that owns it
itsaky-adfa Aug 12, 2026
3dad921
ADFA-4826: Fix misleading KDoc and add else block test
itsaky-adfa Aug 12, 2026
b950110
ADFA-4826: Write the return type when converting an expression body
itsaky-adfa Aug 12, 2026
3a5f798
ADFA-4826: Anchor the declaration in the scope the user picked
itsaky-adfa Aug 12, 2026
b8d0bcc
ADFA-4826: Cover contentSpanOf and fix a nested-block fixture
itsaky-adfa Aug 12, 2026
657d3a0
ADFA-4826: Expand a block written on one line
itsaky-adfa Aug 12, 2026
d565bc1
ADFA-4826: Expand only a block that is really written on one line
itsaky-adfa Aug 12, 2026
2bbf3a3
ADFA-4826: Split the type-text renderer from its catching form
itsaky-adfa Aug 12, 2026
856e0be
ADFA-4826: Decline a block whose statement shares the brace line
itsaky-adfa Aug 12, 2026
458a2ae
ADFA-4826: Tidy the expression-body conversion and its docs
itsaky-adfa Aug 12, 2026
bb09111
ADFA-5080: Add extract-method requirements and ADR 0013
itsaky-adfa Aug 11, 2026
bb9dab1
ADFA-5080: Hoist the shared refactoring plan supertype
itsaky-adfa Aug 10, 2026
60220d2
ADFA-5080: Resolve a selection to an extraction region
itsaky-adfa Aug 10, 2026
513b2d4
ADFA-5080: Fix KDoc and pin fallthrough for extraction region
itsaky-adfa Aug 10, 2026
c1ff95b
ADFA-5080: Add the extract-method plan model and its two rewrites
itsaky-adfa Aug 10, 2026
a8177c1
ADFA-5080: Strengthen the CRLF test and cover the Unit-expression case
itsaky-adfa Aug 10, 2026
83eef14
ADFA-5080: Derive the extracted signature, or a typed refusal
itsaky-adfa Aug 10, 2026
21a1924
ADFA-5080: Close the extract-method refusal gaps that emit broken Kotlin
itsaky-adfa Aug 10, 2026
ceb11bd
ADFA-5080: Refuse the receiver, label and smart-cast cases that emit …
itsaky-adfa Aug 10, 2026
290e3ad
ADFA-5080: Declare a local extracted function before its call site
itsaky-adfa Aug 10, 2026
0db25d2
ADFA-5080: Add the extract-method sheet state and strings
itsaky-adfa Aug 10, 2026
cf52d45
ADFA-5080: Reword two extract-method refusal messages
itsaky-adfa Aug 10, 2026
626d975
ADFA-5080: Add the extract-method Compose sheet
itsaky-adfa Aug 10, 2026
13be859
ADFA-5080: Wire up the extract-method code action
itsaky-adfa Aug 10, 2026
6bc76f7
ADFA-5080: Stop the analysis emitting Kotlin that does not compile
itsaky-adfa Aug 11, 2026
b95542a
ADFA-5080: Revert the qualified-selector capture guard
itsaky-adfa Aug 11, 2026
13df52d
ADFA-5080: Use the shared type-text helpers
itsaky-adfa Aug 12, 2026
e80d4c9
ADFA-4827: Add inline-variable requirements and amend ADR 0013
itsaky-adfa Aug 14, 2026
0f5997c
ADFA-4827: Add the inline-variable plan model and its derived labels
itsaky-adfa Aug 14, 2026
30723aa
ADFA-4827: Document the inline-variable plan model's public members
itsaky-adfa Aug 14, 2026
0bc3935
ADFA-4827: Emit the inline-variable substitutions and declaration del…
itsaky-adfa Aug 14, 2026
5abf21e
ADFA-4827: Resolve the inline-variable target, references and cutoff
itsaky-adfa Aug 14, 2026
89444d7
ADFA-4827: Exclude the references an inline would break
itsaky-adfa Aug 14, 2026
002c0f0
ADFA-4827: Close two shadowing gaps in the inline-variable scope walk
itsaky-adfa Aug 14, 2026
119ac94
ADFA-4827: Add the inline-variable sheet and its strings
itsaky-adfa Aug 14, 2026
915f1d7
ADFA-4827: Wire up the inline-variable code action
itsaky-adfa Aug 14, 2026
a9ef135
ADFA-4827: Apply spotless formatting to the inline-variable files
itsaky-adfa Aug 14, 2026
96f85f4
ADFA-4827: Close the inline-variable soundness gaps found in review
itsaky-adfa Aug 14, 2026
2d113fc
ADFA-4827: Record the inline-variable cutoff limitations
itsaky-adfa Aug 14, 2026
71da532
ADFA-4827: Update the exclusion requirements for the widened rules
itsaky-adfa Aug 14, 2026
a302b4b
ADFA-4827: Bring the inline-variable spec fully in step with the code
itsaky-adfa Aug 14, 2026
f6359f0
ADFA-4827: Correct the shadowing walk and write-detection notes
itsaky-adfa Aug 14, 2026
513ef36
Merge branch 'stage' into feat/ADFA-4827-inline-variable
itsaky-adfa Aug 19, 2026
f8d1bb7
Merge branch 'stage' into feat/ADFA-4827-inline-variable
itsaky-adfa Aug 20, 2026
b964570
Merge branch 'stage' into feat/ADFA-4827-inline-variable
itsaky-adfa Aug 21, 2026
7b01959
ADFA-4827: Resolve the inline target from two more caret positions
itsaky-adfa Aug 21, 2026
ee6fdb7
ADFA-4827: Exclude loop-body references from the inline cutoff
itsaky-adfa Aug 21, 2026
7691fba
ADFA-4827: Detect the receiver shift a class or object body causes
itsaky-adfa Aug 21, 2026
6edf66f
ADFA-4827: Brace keyword initializers substituted into string templates
itsaky-adfa Aug 21, 2026
9577afd
ADFA-4827: Report the inline sheet's dead ends and let it scroll
itsaky-adfa Aug 21, 2026
d22d2c8
ADFA-4827: Bring the inline-variable docs in step with the fixes
itsaky-adfa Aug 21, 2026
8d48adc
ADFA-4827: Defer references inside a local class body too
itsaky-adfa Aug 21, 2026
e888433
ADFA-4827: Widen the exclusion KDoc to what the checks now cover
itsaky-adfa Aug 21, 2026
95ac432
ADFA-5153: Decode Content rows against the shared Brotli dictionary (…
davidschachterADFA Aug 22, 2026
24cc73e
ADFA-4510: Fix missing tooltips on code actions (#1712)
hal-eisen-adfa Aug 24, 2026
7254eda
Merge branch 'stage' into feat/ADFA-4827-inline-variable
itsaky-adfa Aug 25, 2026
7cd47ab
Merge branch 'stage' into feat/ADFA-4827-inline-variable
itsaky-adfa Aug 27, 2026
c3535ad
ADFA-4827: Keep a dismiss that lands before the progress sheet attaches
itsaky-adfa Aug 27, 2026
d9b8d88
Merge branch 'stage' into feat/ADFA-4827-inline-variable
itsaky-adfa Aug 27, 2026
409f48d
ADFA-4827: Keep a trailing comment out of the declaration's span
itsaky-adfa Aug 27, 2026
d52daf6
Merge branch 'stage' into feat/ADFA-4827-inline-variable
itsaky-adfa Aug 28, 2026
a0f1b75
Merge branch 'stage' into feat/ADFA-4827-inline-variable
itsaky-adfa Aug 28, 2026
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 numberDiff line numberDiff line change
Expand Up@@ -30,68 +30,89 @@

public class ProgressSheet extends BaseBottomSheetFragment {

private LayoutProgressSheetBinding binding;
private String message = "";
private String subMessage = "";
private boolean subMessageEnabled = false;

@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);

binding.message.setText(message);

final var params = (ConstraintLayout.LayoutParams) binding.message.getLayoutParams();
if (subMessageEnabled) {
binding.subMessage.setText(subMessage);
binding.subMessage.setVisibility(View.VISIBLE);
params.bottomToBottom = View.NO_ID;
} else {
binding.subMessage.setVisibility(View.GONE);
params.bottomToBottom = LayoutParams.PARENT_ID;
}
}

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState
) {
binding = LayoutProgressSheetBinding.inflate(LayoutInflater.from(getContext()));
return binding.getRoot();
}

public void setSubMessageEnabled(boolean enabled) {
this.subMessageEnabled = enabled;
}

public void setSubMessage(String msg) {
this.subMessage = msg;
if (isShowing()) {
binding.subMessage.setText(msg);
}
}

public ProgressSheet setMessage(String message) {
this.message = message;
if (isShowing()) {
binding.message.setText(message);
}

return this;
}

public ProgressSheet setProgressDrawable(Drawable drawable) {
if (isShowing()) {
binding.progress.setIndeterminateDrawable(drawable);
}
return this;
}

@Override
public void dismiss() {
if (isShowing()) {
super.dismiss();
}
}
private LayoutProgressSheetBinding binding;
private String message = "";
private String subMessage = "";
private boolean subMessageEnabled = false;

/* A dismiss that arrived before this fragment was attached, replayed in onStart. */
private boolean dismissPending = false;

/**
* {@inheritDoc}
*
* <p>
* A dismiss that arrives before the enqueued {@code show()} transaction has run is remembered rather than dropped: the fragment is not attached to a fragment manager yet, so dismissing now would throw, but doing nothing would leave the sheet on screen with nothing left to close it.
*/
@Override
public void dismiss() {
if (!isAdded()) {
dismissPending = true;
return;
}

dismissPending = false;
super.dismiss();
}

@Nullable
@Override
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
binding = LayoutProgressSheetBinding.inflate(LayoutInflater.from(getContext()));
return binding.getRoot();
}

@Override
public void onStart() {
super.onStart();
if (dismissPending) {
dismissPending = false;
dismissAllowingStateLoss();
}
}

@Override
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);

binding.message.setText(message);

final var params = (ConstraintLayout.LayoutParams) binding.message.getLayoutParams();
if (subMessageEnabled) {
binding.subMessage.setText(subMessage);
binding.subMessage.setVisibility(View.VISIBLE);
params.bottomToBottom = View.NO_ID;
} else {
binding.subMessage.setVisibility(View.GONE);
params.bottomToBottom = LayoutParams.PARENT_ID;
}
}

public ProgressSheet setMessage(String message) {
this.message = message;
if (isShowing()) {
binding.message.setText(message);
}

return this;
}

public ProgressSheet setProgressDrawable(Drawable drawable) {
if (isShowing()) {
binding.progress.setIndeterminateDrawable(drawable);
}
return this;
}

public void setSubMessage(String msg) {
this.subMessage = msg;
if (isShowing()) {
binding.subMessage.setText(msg);
}
}

public void setSubMessageEnabled(boolean enabled) {
this.subMessageEnabled = enabled;
}
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/

package com.itsaky.androidide.fragments.sheets

import android.os.Looper
import androidx.appcompat.app.AppCompatActivity
import com.google.common.truth.Truth.assertThat
import com.itsaky.androidide.app.BaseApplication
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.Robolectric
import org.robolectric.RobolectricTestRunner
import org.robolectric.Shadows.shadowOf
import org.robolectric.annotation.Config

/**
* `DialogFragment.show` only enqueues the add transaction, so a dismiss issued in the same
* main-thread pass lands before the sheet exists. Callers that show a progress sheet around work
* that can finish synchronously - `IDELanguageClientImpl.performCodeAction` - rely on that dismiss
* being honoured; dropping it strands the sheet on screen with nothing left to close it.
*/
@RunWith(RobolectricTestRunner::class)
@Config(application = ProgressSheetDismissTest.TestApp::class)
class ProgressSheetDismissTest {
open class TestApp : BaseApplication()

@Test
fun `dismiss issued before the show transaction runs still closes the sheet`() {
val activity = Robolectric.buildActivity(AppCompatActivity::class.java).setup().get()
val manager = activity.supportFragmentManager

val sheet = ProgressSheet()
sheet.isCancelable = false
sheet.show(manager, TAG)
sheet.dismiss()

shadowOf(Looper.getMainLooper()).idle()
manager.executePendingTransactions()

assertThat(manager.findFragmentByTag(TAG)).isNull()
assertThat(sheet.isShowing).isFalse()
}

@Test
fun `dismiss issued once the sheet is on screen closes it`() {
val activity = Robolectric.buildActivity(AppCompatActivity::class.java).setup().get()
val manager = activity.supportFragmentManager

val sheet = ProgressSheet()
sheet.isCancelable = false
sheet.show(manager, TAG)

shadowOf(Looper.getMainLooper()).idle()
manager.executePendingTransactions()
assertThat(sheet.isShowing).isTrue()

sheet.dismiss()

shadowOf(Looper.getMainLooper()).idle()
manager.executePendingTransactions()

assertThat(manager.findFragmentByTag(TAG)).isNull()
assertThat(sheet.isShowing).isFalse()
}

private companion object {
const val TAG = "progress_sheet_test"
}
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
# 0012. Refactoring UI lives in the owning LSP module
# 0013. Refactoring UI lives in the owning LSP module

- **Status:** Proposed
- **Date:** 2026-08-03
Expand Down
14 changes: 10 additions & 4 deletions docs/adr/0014-refactorings-decline-rather-than-rewrite.md
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
# 0013. Interactive refactorings decline rather than rewrite unselected code
# 0014. Interactive refactorings decline rather than rewrite unselected code

- **Status:** Proposed
- **Date:** 2026-08-10
- **Deciders:** Code On The Go team

## Context

The K2 Kotlin LSP is growing a family of interactive refactorings: extract variable (ADFA-4826), extract method (ADFA-5080), inline variable (ADFA-4827), semantic rename (ADFA-4825). [ADR 0012](0012-refactoring-ui-lives-in-the-owning-lsp-module.md) settles where their UI lives and that analysis produces plain data. It says nothing about how capable they should be.
The K2 Kotlin LSP is growing a family of interactive refactorings: extract variable (ADFA-4826), extract method (ADFA-5080), inline variable (ADFA-4827), semantic rename (ADFA-4825). [ADR 0013](0013-refactoring-ui-lives-in-the-owning-lsp-module.md) settles where their UI lives and that analysis produces plain data. It says nothing about how capable they should be.

That question turns out to dominate the requirements. Designing extract method surfaced a run of cases where the transformation the user asked for cannot be performed by *moving* their code - it also needs the moved code's interior edited, or a guess about intent:

Expand All@@ -29,8 +29,11 @@ Concretely:
- **Prefer a stricter rule to a cleverer one** when strictness costs capability and cleverness costs certainty. Extract method refuses a reassigned outer `var` even when the write is provably dead, because proving it needs liveness analysis.
- **Never emit code that does not compile, and avoid emitting code that warns.** The two modifiers extract method *does* add - `suspend` and `@Composable` - are required precisely because omitting them breaks compilation.
- **A refusal is a backlog item, not a dead end.** Where the refused case is common, file it: ADFA-5082 tracks the reassigned-`var` output.
- **Where part of the request is sound, apply that part and say so.** A refactoring has three outcomes, not two: apply, refuse, or **apply partially**. Inline variable (ADFA-4827) is the case that needs the third - a variable whose value is reassigned partway through can be inlined at the references before the write and nowhere after it, so refusing the whole thing would discard a sound transformation of the earlier half. A partial application must report both counts and what it left behind, and it must leave the file compiling on its own, exactly as a full application does. It is not a licence to apply the doubtful part and hope.

This applies to the whole refactoring family, not just extract method. Inline variable and rename inherit it.
This applies to the whole refactoring family, not just extract method. Inline variable and rename inherit it - inline variable adding the third outcome above, rename presumed to need only the first two until its design says otherwise.

**Out of scope of this decision.** Whether a refactoring may duplicate an expression that is evaluated more than once. Inline variable does, without checking for side effects (see [kotlin-inline-variable.md](../features/kotlin-inline-variable.md)): the emitted code compiles, carries no warning, and is the user's own expression unedited, so nothing above forbids it. Kotlin offers no way to prove purity, so a check would be a heuristic rather than a stricter rule, and this ADR prefers strictness to cleverness in both directions.

## Consequences

Expand All@@ -40,12 +43,14 @@ This applies to the whole refactoring family, not just extract method. Inline va
- Refusal reasons are cheap to specify, cheap to test (one case each) and cheap to QA, where a clever transformation needs its own test matrix and its own failure modes.
- The rules are stateable in a sentence each, which is what makes the feature docs reviewable by someone who has not read the implementation.
- Excluding cases by construction keeps the analysis pass small, which matters when it runs on a phone.
- Partial application recovers capability that an all-or-nothing rule would throw away, without weakening the compile-and-do-not-warn guarantee: the sound part is applied and the doubtful part is simply not touched.

**Negative / costs**

- The refactorings are visibly less capable than a desktop IDE's. Two of extract method's refusals - a reassigned outer `var` (the accumulator loop) and an enclosing `with`/`apply` receiver (pervasive in Android code) - will be hit routinely.
- The quality of the *messages* becomes load-bearing. A generic refusal reads as a broken feature, so this decision spends translated strings: roughly seven for extract method alone.
- Users arriving from IntelliJ will read some refusals as regressions rather than as design.
- A partial application is harder to *report* than either other outcome, and harder to QA: the message has to convey two counts and a surviving declaration in one flash, and every "how many were left behind" case is its own test. A partial result the user misreads as a complete one is the failure mode to watch.
- The line is a judgement, not a formalism. "Editing the interior of the moved code" is clear in the cases above but will need re-application, case by case, in each future refactoring.

## Alternatives considered
Expand All@@ -57,7 +62,8 @@ This applies to the whole refactoring family, not just extract method. Inline va

## Related

- [ADR 0012](0012-refactoring-ui-lives-in-the-owning-lsp-module.md) - where refactoring UI lives; this ADR answers *how capable it is*
- [ADR 0013](0013-refactoring-ui-lives-in-the-owning-lsp-module.md) - where refactoring UI lives; this ADR answers *how capable it is*
- [ADR 0010](0010-navigation-resolves-via-analysis-api.md) - the K2 Analysis API as the Kotlin semantic source of truth
- [kotlin-extract-method.md](../features/kotlin-extract-method.md) - R7 to R10 and R14 are this decision applied case by case
- [kotlin-extract-variable.md](../features/kotlin-extract-variable.md) - the shared vocabulary and primitives
- [kotlin-inline-variable.md](../features/kotlin-inline-variable.md) - R6 to R9 are this decision applied to a subtractive refactoring, and the origin of the third outcome
4 changes: 2 additions & 2 deletions docs/adr/README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -26,5 +26,5 @@ Format is lightweight **MADR / Nygard**: Context → Decision → Consequences
| [0010](0010-navigation-resolves-via-analysis-api.md) | Kotlin navigation resolves via the Analysis API, not the symbol index | Proposed |
| [0011](0011-command-analysis-priority.md) | User-invoked commands get their own analysis priority | Proposed |
| [0012](0012-volatile-build-metadata-out-of-abis.md) | Keep volatile build metadata out of module ABIs | Proposed |
| [0013](0012-refactoring-ui-lives-in-the-owning-lsp-module.md) | Refactoring UI lives in the owning LSP module | Proposed |
| [0014](0013-refactorings-decline-rather-than-rewrite.md) | Interactive refactorings decline rather than rewrite unselected code | Proposed |
| [0013](0013-refactoring-ui-lives-in-the-owning-lsp-module.md) | Refactoring UI lives in the owning LSP module | Proposed |
| [0014](0014-refactorings-decline-rather-than-rewrite.md) | Interactive refactorings decline rather than rewrite unselected code | Proposed |
Loading
Loading