Skip to content

Commit 6d07c55

Browse files
ADFA-5069: Replace in project (#2082)
* style: spotless reformat, no functional change * ADFA-5069: Search options: match case, whole word, buffer overrides, scoped walk RecursiveFileSearcher takes a ProjectSearchOptions: - matchCase drops the case-insensitive flags. - wholeWord rejects a match that abuts a word character on an edge where the query itself is a word character, so server3 skips server33 while /server33/ still matches inside a path. - bufferOverrides lets open tabs be searched as they are in memory, not as they are on disk. - excludedDirNames / excludedDirs skip directories before descending, which the project-root scope needs (build/, .gradle/, .git/, and the module dirs already covered by their src entries). The four-argument entry point delegates with defaults, so existing callers behave as before. A synchronous search() backs the async one for tests. * ADFA-5069: Refresh the Kotlin server's copy of files rewritten on disk KtSymbolIndex caches closed files in ktFileCache and nothing ever evicted an entry, so a closed .kt file rewritten on disk kept being analysed as its old text. DiskCopyEvictionTest reproduces that before the fix. New FileContentChangedEvent means "the IDE rewrote this file on disk". The Kotlin server handles it for closed files by evicting the cached KtFile, dropping the PSI view provider, publishing the same modification events file creation uses, and re-indexing. setViewProvider(vf, null) is used because reloadFromDisk and dropPsiCaches both fail in the standalone Analysis API environment (missing tree-change extension point, EDT-only access). * ADFA-5069: Verified multi-file text replacement with disk undo - TextReplacement checks every match still holds its original text, then applies replacements last-to-first so earlier edits never shift later ones. One stale match skips the whole file rather than guessing. - ProjectReplacer writes closed files atomically (temp file + move) and keeps the original bytes for undo. A failed file is reported and the rest still go through. - Undo restores a file only if it still holds exactly the bytes the replace wrote. Content, not lastModified: on /sdcard a rename-over write did not update the modification time, and a timestamp also misses edits within the same tick. - ReplaceSession keys exclusions by file + position, because SearchResult inherits value equality from Range and would otherwise conflate the same position in two files. * ADFA-5069: Replace in project: dialog, preview, apply and undo - "Replace in project" in the Find menu and on Ctrl+Shift+R opens the project search dialog in replace mode: Replace with, Match case (on), Whole word, and Project files outside modules (on). Find in project gains the same options, off by default, and now searches open tabs' unsaved text. - Results show a checkbox per match and a tri-state one per file, the matched text struck through with the replacement beside it, and a bar with the included count, Replace and Cancel. - Open tabs are edited in the buffer as one batch edit and stay unsaved; closed files go through ProjectReplacer. Results are cleared after a replace so no stale match can be opened. - A bar reports the outcome with Undo; skipped and failed files are listed with their reason. - Match rows set baselineAligned=false: with the checkbox beside it, the preview was baseline-shifted and its last line clipped. * fix: address review on ADFA-5069 - Replace ran twice on a double tap: the button now disables on the first tap and the results clear before the replace starts. - Undo restored a file on disk under a tab opened after the replace, so the next save reverted the undo. A reopened, unchanged tab now gets its buffer restored (left unsaved, like replace itself); an edited one is skipped as changed since replace. - build/.git/... name exclusions applied at every depth, hiding source packages such as com/acme/build. They now apply only to the project root walk, still at any depth, so build-logic/build stays excluded. - The preview strikethrough could mark an earlier occurrence of the match. SearchResult now carries the match offset in its preview. - writeAtomically re-checks the target just before the move and copies POSIX permissions onto the temp file when the file system has them, so gradlew keeps its executable bit. Verified on the arm64 emulator: double tap replaces once, undo under a reopened tab restores the buffer, a com/example/build package is found, and each row strikes its own occurrence. * fix: open Find/Replace in project with a fresh dialog, ADFA-5069 Both dialogs were built once and reused until the project re-initialised, so a dismissed dialog came back with the previous query, replacement, filter and checkboxes, and a module list that could be stale after a sync. Each open now builds a new dialog. * fix: address second review on ADFA-5069 - Module build scripts (app/build.gradle.kts, proguard-rules.pro) were searched by neither walk. The project-root walk now excludes each module's src instead of the whole module; app/build stays excluded by the root name exclusions. - A one-character file name crashed replace and undo: File.createTempFile rejects a prefix shorter than 3 characters. The temp prefix is now ".<name>.". - Undo of a file opened in a tab after the replace restored only the buffer, so closing the tab without saving kept the replaced text on disk. It now restores the disk as well, then the buffer, and marks the tab unmodified; if the disk restore is skipped the buffer is untouched. - The replace report's Undo flashbar outlived discardUndo(). It is now kept and dismissed whenever undo data is discarded, and a new report replaces the old one. - Recursive search followed symlinks, which could reach files outside the project or loop. Symlinked files and directories are skipped. - Replace and undo are exclusive: a request while one is running is refused with a message, and the Replace button is re-enabled. Verified on the arm64 emulator: app/build.gradle.kts appears in results, a file named x is replaced without a crash, and undo under a reopened tab restores disk and buffer with the tab left clean. * style: spotless reformat, no functional change Enrolls the four editor-treesitter files the next commits touch. LineSpansGenerator's two license headers become one comment: ktlint rejects the sora-editor header as a dangling KDoc, and two consecutive block comments. The license text is unchanged. * ADFA-5069: Compute tree-sitter spans from the text the tree was parsed from LineSpansGenerator computes spans on a background thread from a tree copy but read the editor's live Content, which the main thread keeps editing. A span job running while edits landed (replace-all in a large open file) read the new text at the old tree's offsets: StringIndexOutOfBoundsException from MatchPredicate. Introduced by #1032 (ADFA-3099). Each generator now reads a snapshot of the worker's text at its tree's version. An edit marks it stale, which is what the deferred tree.edit achieved through hasChanges(), and the generator no longer applies the pooled TSInputEdit after the worker has recycled it. editor androidTest: exclude testing:common from testing:android. It shares :common's coordinates, so the test APK shipped without the production :common classes. * ADFA-5069: Stop analyzer workers without freeing state they still use TsAnalyzeWorker.stop() runs on the main thread and closed the document, parser and tree while the worker thread could still be applying an edit or parsing: IllegalStateException "Cannot access native object" in doMod, and a native abort (Scudo invalid chunk state in ts_parser_delete). It also blocked the main thread until the parse returned. The log panes restarted their analyzer every two seconds and hit this on each restart. stop() now requests cancellation without waiting and wakes the loop with a Stop message; the worker thread releases the document after its current message. * ADFA-5069: Re-sync the log panes once the editor is laid out While a log editor is not laid out, for example with the bottom sheet collapsed after returning to the app, every append timed out after two seconds and re-synced: the whole buffer was rewritten and the analyzer restarted every two seconds while the app kept logging. The re-sync came with #1674 (ADFA-4808). Wait for the layout, then re-sync once, and skip the trim when a re-sync is about to replace the text anyway. * ADFA-5069: Keep a stopped analyzer worker from publishing styles A worker already inside updateStyles() when stop() ran kept going after the manager tore its styles down: it destroyed the current span generator, which rerun() then destroyed again (RejectedExecutionException on the calling thread), leaked the generator it built and its bracket pairs, and could publish stale styles over the new worker's. The generator, bracket pairs and code blocks are still built without a lock; publishing them now happens under the worker's lock and re-checks isDestroyed, disposing of them when the worker has stopped. stop() sets the flag under the same lock, which only ever covers assignments and posts, so the main thread still never waits on a parse or a query. stop() also requests cancellation before setting isDestroyed, so the worker cannot close the parser during that native call. * style: spotless reformat, no functional change --------- Co-authored-by: Daniel Alome <astrocoder007@gmail.com>
1 parent e684049 commit 6d07c55

44 files changed

Lines changed: 3224 additions & 1218 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package com.itsaky.androidide.actions.etc
2+
3+
import android.content.Context
4+
import androidx.core.content.ContextCompat
5+
import com.itsaky.androidide.actions.ActionData
6+
import com.itsaky.androidide.actions.ActionItem
7+
import com.itsaky.androidide.actions.EditorActivityAction
8+
import com.itsaky.androidide.actions.markInvisible
9+
import com.itsaky.androidide.idetooltips.TooltipTag
10+
import com.itsaky.androidide.projects.IProjectManager
11+
import com.itsaky.androidide.resources.R
12+
13+
class ReplaceInProjectAction() : EditorActivityAction() {
14+
override val id: String = ID
15+
override var requiresUIThread: Boolean = true
16+
override var order: Int = 0
17+
override var location: ActionItem.Location = ActionItem.Location.EDITOR_FIND_ACTION_MENU
18+
19+
override fun retrieveTooltipTag(isReadOnlyContext: Boolean): String = TooltipTag.EDITOR_TOOLBAR_FIND_IN_PROJECT
20+
21+
companion object {
22+
const val ID = "ide.editor.replace.inProject"
23+
}
24+
25+
constructor(context: Context, order: Int) : this() {
26+
this.label = context.getString(R.string.menu_replace_project)
27+
this.icon = ContextCompat.getDrawable(context, R.drawable.ic_search_project)
28+
this.order = order
29+
}
30+
31+
override fun prepare(data: ActionData) {
32+
super.prepare(data)
33+
data.getActivity()
34+
?: run {
35+
markInvisible()
36+
return
37+
}
38+
39+
val gradleBuild = IProjectManager.getInstance().gradleBuild
40+
if (gradleBuild == null || gradleBuild.subProjectCount == 0) {
41+
markInvisible()
42+
return
43+
}
44+
45+
visible = true
46+
enabled = true
47+
}
48+
49+
override suspend fun execAction(data: ActionData): Boolean {
50+
val context = data.getActivity() ?: return false
51+
val dialog = context.replaceInProjectDialog ?: return false
52+
53+
return run {
54+
dialog.show()
55+
true
56+
}
57+
}
58+
}

‎app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,6 +994,10 @@ open class EditorHandlerActivity :
994994
string.cd_toolbar_find_in_project
995995
}
996996

997+
"ide.editor.replace.inProject" -> {
998+
string.cd_toolbar_replace_in_project
999+
}
1000+
9971001
"ide.editor.launchInstalledApp" -> {
9981002
string.cd_toolbar_launch_app
9991003
}

0 commit comments

Comments
 (0)