From 79c13d658ae919b1f77d4612807492117a12fee4 Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Fri, 27 Feb 2026 15:29:58 +0100 Subject: [PATCH 01/10] feat(ADFA-2879): Add Git tab to editor bottom sheet --- .../adapters/EditorBottomSheetTabAdapter.kt | 10 ++++++++ .../fragments/git/GitBottomSheetFragment.kt | 23 ++++++++++++++++++ .../viewmodel/BottomSheetViewModel.kt | 2 ++ .../res/layout/fragment_git_bottom_sheet.xml | 24 +++++++++++++++++++ resources/src/main/res/values/strings.xml | 2 ++ 5 files changed, 61 insertions(+) create mode 100644 app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt create mode 100644 app/src/main/res/layout/fragment_git_bottom_sheet.xml diff --git a/app/src/main/java/com/itsaky/androidide/adapters/EditorBottomSheetTabAdapter.kt b/app/src/main/java/com/itsaky/androidide/adapters/EditorBottomSheetTabAdapter.kt index 26fffbf151..629ef01548 100644 --- a/app/src/main/java/com/itsaky/androidide/adapters/EditorBottomSheetTabAdapter.kt +++ b/app/src/main/java/com/itsaky/androidide/adapters/EditorBottomSheetTabAdapter.kt @@ -25,6 +25,7 @@ import com.itsaky.androidide.app.IDEApplication.Companion.getPluginManager import com.itsaky.androidide.fragments.DiagnosticsListFragment import com.itsaky.androidide.fragments.SearchResultFragment import com.itsaky.androidide.fragments.debug.DebuggerFragment +import com.itsaky.androidide.fragments.git.GitBottomSheetFragment import com.itsaky.androidide.fragments.output.AppLogFragment import com.itsaky.androidide.fragments.output.BuildOutputFragment import com.itsaky.androidide.fragments.output.IDELogFragment @@ -53,6 +54,7 @@ class EditorBottomSheetTabAdapter( const val TAB_SEARCH_RESULTS = 4 const val TAB_DEBUGGER = 5 const val TAB_AGENT = 6 + const val TAB_GIT = 7 } private val allTabs = @@ -120,6 +122,14 @@ class EditorBottomSheetTabAdapter( tooltipTag = TooltipTag.PROJECT_AGENT, ), ) + + add( + Tab( + title = fragmentActivity.getString(R.string.git_title), + fragmentClass = GitBottomSheetFragment::class.java, + itemId = TAB_GIT, + ), + ) } } diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt new file mode 100644 index 0000000000..e5cd26cffb --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt @@ -0,0 +1,23 @@ +package com.itsaky.androidide.fragments.git + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import com.itsaky.androidide.R +import com.itsaky.androidide.databinding.FragmentGitBottomSheetBinding + +class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { + + private var _binding: FragmentGitBottomSheetBinding? = null + private val binding get() = _binding!! + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + _binding = FragmentGitBottomSheetBinding.bind(view) + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } +} diff --git a/app/src/main/java/com/itsaky/androidide/viewmodel/BottomSheetViewModel.kt b/app/src/main/java/com/itsaky/androidide/viewmodel/BottomSheetViewModel.kt index bff0336eb9..bda6632cfd 100644 --- a/app/src/main/java/com/itsaky/androidide/viewmodel/BottomSheetViewModel.kt +++ b/app/src/main/java/com/itsaky/androidide/viewmodel/BottomSheetViewModel.kt @@ -31,6 +31,7 @@ class BottomSheetViewModel : ViewModel() { const val TAB_SEARCH_RESULT = EditorBottomSheetTabAdapter.TAB_SEARCH_RESULTS const val TAB_DEBUGGER = EditorBottomSheetTabAdapter.TAB_DEBUGGER const val TAB_AGENT = EditorBottomSheetTabAdapter.TAB_AGENT + const val TAB_GIT = EditorBottomSheetTabAdapter.TAB_GIT } @Keep @@ -42,6 +43,7 @@ class BottomSheetViewModel : ViewModel() { TAB_SEARCH_RESULT, TAB_DEBUGGER, TAB_AGENT, + TAB_GIT, ) @Retention(AnnotationRetention.SOURCE) annotation class TabDef diff --git a/app/src/main/res/layout/fragment_git_bottom_sheet.xml b/app/src/main/res/layout/fragment_git_bottom_sheet.xml new file mode 100644 index 0000000000..55b8249df5 --- /dev/null +++ b/app/src/main/res/layout/fragment_git_bottom_sheet.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml index ce2302ae66..7a348b13de 100644 --- a/resources/src/main/res/values/strings.xml +++ b/resources/src/main/res/values/strings.xml @@ -1161,4 +1161,6 @@ Clone failed: %1$s Cloning repository… Pick folder + Git + No uncommitted changes From 480f67798f1310ce4696f87247f5d8e22b63480c Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Fri, 27 Feb 2026 16:04:51 +0100 Subject: [PATCH 02/10] feat(ADFA-2879): List modified files --- .../com/itsaky/androidide/di/AppModule.kt | 3 + .../fragments/git/GitBottomSheetFragment.kt | 32 ++++++++ .../git/adapter/GitFileChangeAdapter.kt | 81 +++++++++++++++++++ .../viewmodel/GitBottomSheetViewModel.kt | 67 +++++++++++++++ app/src/main/res/drawable/ic_file_added.xml | 5 ++ .../main/res/drawable/ic_file_conflicted.xml | 11 +++ app/src/main/res/drawable/ic_file_deleted.xml | 11 +++ .../main/res/drawable/ic_file_modified.xml | 11 +++ app/src/main/res/drawable/ic_file_renamed.xml | 11 +++ .../main/res/layout/item_git_file_change.xml | 36 +++++++++ 10 files changed, 268 insertions(+) create mode 100644 app/src/main/java/com/itsaky/androidide/fragments/git/adapter/GitFileChangeAdapter.kt create mode 100644 app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt create mode 100644 app/src/main/res/drawable/ic_file_added.xml create mode 100644 app/src/main/res/drawable/ic_file_conflicted.xml create mode 100644 app/src/main/res/drawable/ic_file_deleted.xml create mode 100644 app/src/main/res/drawable/ic_file_modified.xml create mode 100644 app/src/main/res/drawable/ic_file_renamed.xml create mode 100644 app/src/main/res/layout/item_git_file_change.xml diff --git a/app/src/main/java/com/itsaky/androidide/di/AppModule.kt b/app/src/main/java/com/itsaky/androidide/di/AppModule.kt index f3c46cc425..39e66563fd 100644 --- a/app/src/main/java/com/itsaky/androidide/di/AppModule.kt +++ b/app/src/main/java/com/itsaky/androidide/di/AppModule.kt @@ -19,4 +19,7 @@ val coreModule = viewModel { ChatViewModel() } + viewModel { + com.itsaky.androidide.viewmodel.GitBottomSheetViewModel() + } } diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt index e5cd26cffb..918fc98fb8 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt @@ -3,17 +3,49 @@ package com.itsaky.androidide.fragments.git import android.os.Bundle import android.view.View import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager import com.itsaky.androidide.R import com.itsaky.androidide.databinding.FragmentGitBottomSheetBinding +import com.itsaky.androidide.fragments.git.adapter.GitFileChangeAdapter +import com.itsaky.androidide.viewmodel.GitBottomSheetViewModel +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch +import org.koin.androidx.viewmodel.ext.android.viewModel class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { + private val viewModel: GitBottomSheetViewModel by viewModel() + private lateinit var adapter: GitFileChangeAdapter + private var _binding: FragmentGitBottomSheetBinding? = null private val binding get() = _binding!! override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) _binding = FragmentGitBottomSheetBinding.bind(view) + + adapter = GitFileChangeAdapter { + // TODO() View diff + } + + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.gitStatus.collectLatest { status -> + val allChanges = status.staged + status.unstaged + status.untracked + status.conflicted + + if (allChanges.isEmpty()) { + binding.emptyView.visibility = View.VISIBLE + binding.recyclerView.visibility = View.GONE + } else { + binding.emptyView.visibility = View.GONE + binding.recyclerView.visibility = View.VISIBLE + adapter.submitList(allChanges) + } + } + } } override fun onDestroyView() { diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/adapter/GitFileChangeAdapter.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/adapter/GitFileChangeAdapter.kt new file mode 100644 index 0000000000..6c7debbba6 --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/adapter/GitFileChangeAdapter.kt @@ -0,0 +1,81 @@ +package com.itsaky.androidide.fragments.git.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.itsaky.androidide.R +import com.itsaky.androidide.databinding.ItemGitFileChangeBinding +import com.itsaky.androidide.git.core.models.ChangeType +import com.itsaky.androidide.git.core.models.FileChange + +class GitFileChangeAdapter( + private val onFileClicked: (FileChange) -> Unit +) : ListAdapter(DiffCallback()) { + + // Keep track of which files are selected to be committed + val selectedFiles = mutableSetOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val binding = ItemGitFileChangeBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + return ViewHolder(binding) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val change = getItem(position) + holder.bind(change) + } + + inner class ViewHolder(private val binding: ItemGitFileChangeBinding) : RecyclerView.ViewHolder(binding.root) { + + init { + itemView.setOnClickListener { + val pos = bindingAdapterPosition + if (pos != RecyclerView.NO_POSITION) { + onFileClicked(getItem(pos)) + } + } + + binding.checkbox.setOnCheckedChangeListener { _, isChecked -> + val pos = bindingAdapterPosition + if (pos != RecyclerView.NO_POSITION) { + val change = getItem(pos) + if (isChecked) { + selectedFiles.add(change.path) + } else { + selectedFiles.remove(change.path) + } + } + } + } + + fun bind(change: FileChange) { + binding.filePath.text = change.path + + binding.checkbox.isChecked = selectedFiles.contains(change.path) + + val imageRes = when (change.type) { + ChangeType.ADDED -> R.drawable.ic_file_added + ChangeType.MODIFIED -> R.drawable.ic_file_modified + ChangeType.DELETED -> R.drawable.ic_file_deleted + ChangeType.UNTRACKED -> R.drawable.ic_file_added + ChangeType.RENAMED -> R.drawable.ic_file_renamed + ChangeType.CONFLICTED -> R.drawable.ic_file_conflicted + } + binding.statusIcon.setImageResource(imageRes) + } + } + + class DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: FileChange, newItem: FileChange): Boolean { + return oldItem.path == newItem.path + } + + override fun areContentsTheSame(oldItem: FileChange, newItem: FileChange): Boolean { + return oldItem == newItem + } + } +} diff --git a/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt b/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt new file mode 100644 index 0000000000..5197d38bae --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt @@ -0,0 +1,67 @@ +package com.itsaky.androidide.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.itsaky.androidide.eventbus.events.editor.DocumentSaveEvent +import com.itsaky.androidide.events.ListProjectFilesRequestEvent +import com.itsaky.androidide.git.core.GitRepository +import com.itsaky.androidide.git.core.GitRepositoryManager +import com.itsaky.androidide.git.core.models.GitStatus +import com.itsaky.androidide.projects.IProjectManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import java.io.File + +class GitBottomSheetViewModel : ViewModel() { + + private val _gitStatus = MutableStateFlow(GitStatus.EMPTY) + val gitStatus: StateFlow = _gitStatus.asStateFlow() + + private var currentRepository: GitRepository? = null + + init { + EventBus.getDefault().register(this) + initializeRepository() + } + + override fun onCleared() { + super.onCleared() + EventBus.getDefault().unregister(this) + currentRepository?.close() + } + + private fun initializeRepository() { + viewModelScope.launch { + val projectDir = File(IProjectManager.getInstance().projectDirPath) + currentRepository = GitRepositoryManager.openRepository(projectDir) + refreshStatus() + } + } + + /** + * Refreshes the Git status of the project. + */ + fun refreshStatus() { + viewModelScope.launch { + currentRepository?.let { repo -> + val status = repo.getStatus() + _gitStatus.value = status + } + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onDocumentSaved(event: DocumentSaveEvent) { + refreshStatus() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onProjectFilesChanged(event: ListProjectFilesRequestEvent) { + refreshStatus() + } +} diff --git a/app/src/main/res/drawable/ic_file_added.xml b/app/src/main/res/drawable/ic_file_added.xml new file mode 100644 index 0000000000..2c18d57bc0 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_added.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_file_conflicted.xml b/app/src/main/res/drawable/ic_file_conflicted.xml new file mode 100644 index 0000000000..b8a26a680f --- /dev/null +++ b/app/src/main/res/drawable/ic_file_conflicted.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_deleted.xml b/app/src/main/res/drawable/ic_file_deleted.xml new file mode 100644 index 0000000000..c3308c3b14 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_deleted.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_modified.xml b/app/src/main/res/drawable/ic_file_modified.xml new file mode 100644 index 0000000000..f21f6d331c --- /dev/null +++ b/app/src/main/res/drawable/ic_file_modified.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_renamed.xml b/app/src/main/res/drawable/ic_file_renamed.xml new file mode 100644 index 0000000000..b4bdc5c068 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_renamed.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/layout/item_git_file_change.xml b/app/src/main/res/layout/item_git_file_change.xml new file mode 100644 index 0000000000..a715843686 --- /dev/null +++ b/app/src/main/res/layout/item_git_file_change.xml @@ -0,0 +1,36 @@ + + + + + + + + + + From fcd536c1115ad7190bf309ea9da7703723081a6f Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Tue, 3 Mar 2026 12:58:11 +0100 Subject: [PATCH 03/10] feat(ADFA-2879): Implement a unified diff viewer --- .../fragments/git/GitBottomSheetFragment.kt | 14 +- .../fragments/git/GitDiffViewerDialog.kt | 150 ++++++++++++++++++ .../viewmodel/GitBottomSheetViewModel.kt | 3 +- app/src/main/res/layout/dialog_git_diff.xml | 75 +++++++++ .../androidide/git/core/JGitRepository.kt | 42 ++++- resources/src/main/res/values/colors.xml | 8 + resources/src/main/res/values/strings.xml | 2 + 7 files changed, 288 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/com/itsaky/androidide/fragments/git/GitDiffViewerDialog.kt create mode 100644 app/src/main/res/layout/dialog_git_diff.xml diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt index 918fc98fb8..40e877f951 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt @@ -12,6 +12,7 @@ import com.itsaky.androidide.viewmodel.GitBottomSheetViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.ext.android.viewModel +import java.io.File class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { @@ -25,9 +26,16 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { super.onViewCreated(view, savedInstanceState) _binding = FragmentGitBottomSheetBinding.bind(view) - adapter = GitFileChangeAdapter { - // TODO() View diff - } + adapter = GitFileChangeAdapter(onFileClicked = { change -> + val file = File(viewModel.currentRepository?.rootDir, change.path) + viewLifecycleOwner.lifecycleScope.launch { + val diffText = viewModel.currentRepository?.getDiff(file) + ?: getString(R.string.unable_to_load_diff) + val dialog = GitDiffViewerDialog.newInstance(change.path, diffText) + // Show diff in a dialog when changed file is clicked + dialog.show(childFragmentManager, "GitDiffViewerDialog") + } + }) binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) binding.recyclerView.adapter = adapter diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/GitDiffViewerDialog.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/GitDiffViewerDialog.kt new file mode 100644 index 0000000000..f5f7d3b90b --- /dev/null +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/GitDiffViewerDialog.kt @@ -0,0 +1,150 @@ +package com.itsaky.androidide.fragments.git + +import android.app.Dialog +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.os.Bundle +import android.text.Spannable +import android.text.SpannableString +import android.text.SpannableStringBuilder +import android.text.style.BackgroundColorSpan +import android.text.style.ForegroundColorSpan +import android.text.style.LineBackgroundSpan +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.content.ContextCompat +import androidx.fragment.app.DialogFragment +import com.google.android.material.button.MaterialButton +import com.itsaky.androidide.R + +import com.itsaky.androidide.databinding.DialogGitDiffBinding + +class GitDiffViewerDialog : DialogFragment() { + + private var diffText: String = "" + private var filePath: String = "" + + private var _binding: DialogGitDiffBinding? = null + private val binding get() = _binding!! + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.Theme_AndroidIDE) + diffText = arguments?.getString(ARG_DIFF_TEXT) ?: "" + filePath = arguments?.getString(ARG_FILE_PATH) ?: getString(R.string.diff_viewer) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = DialogGitDiffBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.diffTitle.text = filePath + binding.diffText.text = applyDiffFormatting(diffText) + + binding.btnClose.setOnClickListener { + dismiss() + } + } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } + + private fun applyDiffFormatting(diff: String): SpannableStringBuilder { + val builder = SpannableStringBuilder() + val lines = diff.split("\n") + + // Find the index of the first diff chunk (starts with @@) + val firstChunkIndex = lines.indexOfFirst { it.startsWith("@@") } + val startIndex = if (firstChunkIndex != -1) firstChunkIndex else 0 + + val context = requireContext() + val colorAdd = ContextCompat.getColor(context, R.color.git_diff_add_text) + val bgAdd = ContextCompat.getColor(context, R.color.git_diff_add_bg) + val colorDel = ContextCompat.getColor(context, R.color.git_diff_del_text) + val bgDel = ContextCompat.getColor(context, R.color.git_diff_del_bg) + val colorHeader = ContextCompat.getColor(context, R.color.git_diff_header_text) + + for (i in startIndex until lines.size) { + val line = lines[i] + val startIdx = builder.length + when { + line.startsWith("+") && !line.startsWith("+++") -> { + val formattedLine = line.replaceFirst("+", "+ ") + builder.append(formattedLine).append("\n") + val endIdx = builder.length + + builder.setSpan(ForegroundColorSpan(colorAdd), startIdx, endIdx, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + builder.setSpan(FullWidthBackgroundColorSpan(bgAdd), startIdx, endIdx, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + line.startsWith("-") && !line.startsWith("---") -> { + val formattedLine = line.replaceFirst("-", "- ") + builder.append(formattedLine).append("\n") + val endIdx = builder.length + + builder.setSpan(ForegroundColorSpan(colorDel), startIdx, endIdx, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + builder.setSpan(FullWidthBackgroundColorSpan(bgDel), startIdx, endIdx, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + line.startsWith("@@") -> { + builder.append(line).append("\n") + val endIdx = builder.length + + builder.setSpan(ForegroundColorSpan(colorHeader), startIdx, endIdx, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + else -> { + val formattedLine = if (line.startsWith(" ")) " $line" else line + builder.append(formattedLine).append("\n") + } + } + } + + return builder + } + + private class FullWidthBackgroundColorSpan(private val color: Int) : LineBackgroundSpan { + override fun drawBackground( + canvas: Canvas, paint: Paint, + left: Int, right: Int, top: Int, baseline: Int, bottom: Int, + text: CharSequence, start: Int, end: Int, lineNumber: Int + ) { + val oldColor = paint.color + paint.color = color + canvas.drawRect(left.toFloat(), top.toFloat(), right.toFloat(), bottom.toFloat(), paint) + paint.color = oldColor + } + } + + override fun onStart() { + super.onStart() + dialog?.window?.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } + + companion object { + private const val ARG_DIFF_TEXT = "arg_diff_text" + private const val ARG_FILE_PATH = "arg_file_path" + + fun newInstance(filePath: String, diffText: String): GitDiffViewerDialog { + return GitDiffViewerDialog().apply { + arguments = Bundle().apply { + putString(ARG_FILE_PATH, filePath) + putString(ARG_DIFF_TEXT, diffText) + } + } + } + } +} diff --git a/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt b/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt index 5197d38bae..cb6f5eb412 100644 --- a/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt +++ b/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt @@ -22,7 +22,8 @@ class GitBottomSheetViewModel : ViewModel() { private val _gitStatus = MutableStateFlow(GitStatus.EMPTY) val gitStatus: StateFlow = _gitStatus.asStateFlow() - private var currentRepository: GitRepository? = null + var currentRepository: GitRepository? = null + private set init { EventBus.getDefault().register(this) diff --git a/app/src/main/res/layout/dialog_git_diff.xml b/app/src/main/res/layout/dialog_git_diff.xml new file mode 100644 index 0000000000..6b59bea643 --- /dev/null +++ b/app/src/main/res/layout/dialog_git_diff.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt b/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt index 50bcb752a3..f622cf012e 100644 --- a/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt +++ b/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt @@ -11,6 +11,16 @@ import java.io.File import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.eclipse.jgit.api.errors.NoHeadException +import org.eclipse.jgit.diff.DiffFormatter +import org.eclipse.jgit.dircache.DirCacheIterator +import org.eclipse.jgit.revwalk.RevWalk +import org.eclipse.jgit.treewalk.AbstractTreeIterator +import org.eclipse.jgit.treewalk.CanonicalTreeParser +import org.eclipse.jgit.treewalk.EmptyTreeIterator +import org.eclipse.jgit.treewalk.FileTreeIterator +import org.eclipse.jgit.treewalk.filter.PathFilter +import java.io.ByteArrayOutputStream /** * JGit-based implementation of the [GitRepository] interface. @@ -24,6 +34,17 @@ class JGitRepository(override val rootDir: File) : GitRepository { private val git: Git = Git(repository) + private fun getHeadTree(repository: Repository): AbstractTreeIterator { + val head = repository.resolve(Constants.HEAD) ?: return EmptyTreeIterator() + val revWalk = RevWalk(repository) + val commit = revWalk.parseCommit(head) + val treeParser = CanonicalTreeParser() + repository.newObjectReader().use { reader -> + treeParser.reset(reader, commit.tree.id) + } + return treeParser + } + override suspend fun getStatus(): GitStatus = withContext(Dispatchers.IO) { val jgitStatus = git.status().call() @@ -43,6 +64,8 @@ class JGitRepository(override val rootDir: File) : GitRepository { jgitStatus.conflicting.forEach { conflicted.add(FileChange(it, ChangeType.CONFLICTED)) } + + GitStatus( isClean = jgitStatus.isClean, hasConflicts = jgitStatus.conflicting.isNotEmpty(), @@ -81,13 +104,28 @@ class JGitRepository(override val rootDir: File) : GitRepository { git.log().setMaxCount(limit).call().map { revCommit -> revCommit.toGitCommit() } - } catch (_: org.eclipse.jgit.api.errors.NoHeadException) { + } catch (_: NoHeadException) { emptyList() } } override suspend fun getDiff(file: File): String = withContext(Dispatchers.IO) { - "" + val relativePath = file.toRelativeString(rootDir).replace('\\', '/') + val outputStream = ByteArrayOutputStream() + DiffFormatter(outputStream).use { formatter -> + formatter.setRepository(repository) + val indexTree = DirCacheIterator(repository.readDirCache()) + val workingTree = FileTreeIterator(repository) + formatter.pathFilter = PathFilter.create(relativePath) + formatter.format(indexTree, workingTree) + + // If empty, check staged diff + if (outputStream.size() == 0) { + val headTree = getHeadTree(repository) + formatter.format(headTree, indexTree) + } + } + outputStream.toString() } private fun RevCommit.toGitCommit(): GitCommit { diff --git a/resources/src/main/res/values/colors.xml b/resources/src/main/res/values/colors.xml index 3f393c9dde..3019563276 100755 --- a/resources/src/main/res/values/colors.xml +++ b/resources/src/main/res/values/colors.xml @@ -29,4 +29,12 @@ #233490 #E4E2E6 #00BE00 + + + #1B5E20 + #E8F5E9 + #B71C1C + #FFEBEE + #0277BD + diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml index 7a348b13de..874faf5230 100644 --- a/resources/src/main/res/values/strings.xml +++ b/resources/src/main/res/values/strings.xml @@ -1163,4 +1163,6 @@ Pick folder Git No uncommitted changes + Unable to load diff + Diff Viewer From 926c9ec58fadb1e8d47845cea3998278533ed07d Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Fri, 6 Mar 2026 11:37:28 +0100 Subject: [PATCH 04/10] fix(ADFA-2879): Handle diff in viewmodel, not bundle --- .../fragments/git/GitBottomSheetFragment.kt | 16 +++----- .../fragments/git/GitDiffViewerDialog.kt | 38 ++++++++++++------- resources/src/main/res/values/strings.xml | 1 + 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt index 40e877f951..04647d53a8 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/GitBottomSheetFragment.kt @@ -11,12 +11,11 @@ import com.itsaky.androidide.fragments.git.adapter.GitFileChangeAdapter import com.itsaky.androidide.viewmodel.GitBottomSheetViewModel import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch -import org.koin.androidx.viewmodel.ext.android.viewModel -import java.io.File +import androidx.fragment.app.activityViewModels class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { - private val viewModel: GitBottomSheetViewModel by viewModel() + private val viewModel: GitBottomSheetViewModel by activityViewModels() private lateinit var adapter: GitFileChangeAdapter private var _binding: FragmentGitBottomSheetBinding? = null @@ -27,14 +26,9 @@ class GitBottomSheetFragment : Fragment(R.layout.fragment_git_bottom_sheet) { _binding = FragmentGitBottomSheetBinding.bind(view) adapter = GitFileChangeAdapter(onFileClicked = { change -> - val file = File(viewModel.currentRepository?.rootDir, change.path) - viewLifecycleOwner.lifecycleScope.launch { - val diffText = viewModel.currentRepository?.getDiff(file) - ?: getString(R.string.unable_to_load_diff) - val dialog = GitDiffViewerDialog.newInstance(change.path, diffText) - // Show diff in a dialog when changed file is clicked - dialog.show(childFragmentManager, "GitDiffViewerDialog") - } + // Show diff in a dialog when changed file is clicked + val dialog = GitDiffViewerDialog.newInstance(change.path) + dialog.show(childFragmentManager, "GitDiffViewerDialog") }) binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/GitDiffViewerDialog.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/GitDiffViewerDialog.kt index f5f7d3b90b..2d14ff7c74 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/git/GitDiffViewerDialog.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/GitDiffViewerDialog.kt @@ -1,30 +1,29 @@ package com.itsaky.androidide.fragments.git -import android.app.Dialog import android.graphics.Canvas -import android.graphics.Color import android.graphics.Paint import android.os.Bundle import android.text.Spannable -import android.text.SpannableString import android.text.SpannableStringBuilder -import android.text.style.BackgroundColorSpan import android.text.style.ForegroundColorSpan import android.text.style.LineBackgroundSpan import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView import androidx.core.content.ContextCompat import androidx.fragment.app.DialogFragment -import com.google.android.material.button.MaterialButton +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope import com.itsaky.androidide.R - import com.itsaky.androidide.databinding.DialogGitDiffBinding +import com.itsaky.androidide.viewmodel.GitBottomSheetViewModel +import kotlinx.coroutines.launch +import java.io.File class GitDiffViewerDialog : DialogFragment() { - private var diffText: String = "" + private val viewModel: GitBottomSheetViewModel by activityViewModels() + private var filePath: String = "" private var _binding: DialogGitDiffBinding? = null @@ -33,7 +32,6 @@ class GitDiffViewerDialog : DialogFragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setStyle(STYLE_NORMAL, R.style.Theme_AndroidIDE) - diffText = arguments?.getString(ARG_DIFF_TEXT) ?: "" filePath = arguments?.getString(ARG_FILE_PATH) ?: getString(R.string.diff_viewer) } @@ -50,7 +48,23 @@ class GitDiffViewerDialog : DialogFragment() { super.onViewCreated(view, savedInstanceState) binding.diffTitle.text = filePath - binding.diffText.text = applyDiffFormatting(diffText) + binding.diffText.text = getString(R.string.diff_loading) + + viewLifecycleOwner.lifecycleScope.launch(kotlinx.coroutines.Dispatchers.IO) { + val repo = viewModel.currentRepository + val diff = if (repo != null && repo.rootDir.exists()) { + val file = File(repo.rootDir, filePath) + repo.getDiff(file) + } else { + null + } ?: getString(R.string.unable_to_load_diff) + + kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Main) { + if (_binding != null) { + binding.diffText.text = applyDiffFormatting(diff) + } + } + } binding.btnClose.setOnClickListener { dismiss() @@ -135,14 +149,12 @@ class GitDiffViewerDialog : DialogFragment() { } companion object { - private const val ARG_DIFF_TEXT = "arg_diff_text" private const val ARG_FILE_PATH = "arg_file_path" - fun newInstance(filePath: String, diffText: String): GitDiffViewerDialog { + fun newInstance(filePath: String): GitDiffViewerDialog { return GitDiffViewerDialog().apply { arguments = Bundle().apply { putString(ARG_FILE_PATH, filePath) - putString(ARG_DIFF_TEXT, diffText) } } } diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml index 874faf5230..bd8a91983c 100644 --- a/resources/src/main/res/values/strings.xml +++ b/resources/src/main/res/values/strings.xml @@ -1165,4 +1165,5 @@ No uncommitted changes Unable to load diff Diff Viewer + Loading Git Diff… From 90cc17370f3cf858a1b476927310d6b2412d991f Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Fri, 6 Mar 2026 12:26:11 +0100 Subject: [PATCH 05/10] feat(ADFA-2879): Add content description to file status icon --- .../git/adapter/GitFileChangeAdapter.kt | 19 +++++++++++-------- app/src/main/res/values/strings.xml | 6 ++++++ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/adapter/GitFileChangeAdapter.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/adapter/GitFileChangeAdapter.kt index 6c7debbba6..6b679d10f7 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/git/adapter/GitFileChangeAdapter.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/adapter/GitFileChangeAdapter.kt @@ -57,15 +57,18 @@ class GitFileChangeAdapter( binding.checkbox.isChecked = selectedFiles.contains(change.path) - val imageRes = when (change.type) { - ChangeType.ADDED -> R.drawable.ic_file_added - ChangeType.MODIFIED -> R.drawable.ic_file_modified - ChangeType.DELETED -> R.drawable.ic_file_deleted - ChangeType.UNTRACKED -> R.drawable.ic_file_added - ChangeType.RENAMED -> R.drawable.ic_file_renamed - ChangeType.CONFLICTED -> R.drawable.ic_file_conflicted + val (imageRes, descRes) = when (change.type) { + ChangeType.ADDED -> R.drawable.ic_file_added to R.string.desc_file_added + ChangeType.MODIFIED -> R.drawable.ic_file_modified to R.string.desc_file_modified + ChangeType.DELETED -> R.drawable.ic_file_deleted to R.string.desc_file_deleted + ChangeType.UNTRACKED -> R.drawable.ic_file_added to R.string.desc_file_untracked + ChangeType.RENAMED -> R.drawable.ic_file_renamed to R.string.desc_file_renamed + ChangeType.CONFLICTED -> R.drawable.ic_file_conflicted to R.string.desc_file_conflicted + } + binding.statusIcon.apply { + setImageResource(imageRes) + contentDescription = binding.root.context.getString(descRes) } - binding.statusIcon.setImageResource(imageRes) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 563068d8d0..e728ab5e68 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -19,4 +19,10 @@ Code on the Go Use simplified prompt + File added + File modified + File deleted + File untracked + File renamed + File conflicted From c1a1e52b456762880fefff5321f3d92f4582c54b Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Fri, 6 Mar 2026 12:42:51 +0100 Subject: [PATCH 06/10] fix(ADFA-2879): Fix gid diff issues --- .../com/itsaky/androidide/git/core/JGitRepository.kt | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt b/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt index f622cf012e..791c502044 100644 --- a/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt +++ b/git-core/src/main/java/com/itsaky/androidide/git/core/JGitRepository.kt @@ -36,11 +36,12 @@ class JGitRepository(override val rootDir: File) : GitRepository { private fun getHeadTree(repository: Repository): AbstractTreeIterator { val head = repository.resolve(Constants.HEAD) ?: return EmptyTreeIterator() - val revWalk = RevWalk(repository) - val commit = revWalk.parseCommit(head) val treeParser = CanonicalTreeParser() - repository.newObjectReader().use { reader -> - treeParser.reset(reader, commit.tree.id) + RevWalk(repository).use { revWalk -> + val commit = revWalk.parseCommit(head) + repository.newObjectReader().use { reader -> + treeParser.reset(reader, commit.tree.id) + } } return treeParser } @@ -122,7 +123,8 @@ class JGitRepository(override val rootDir: File) : GitRepository { // If empty, check staged diff if (outputStream.size() == 0) { val headTree = getHeadTree(repository) - formatter.format(headTree, indexTree) + val freshIndexTree = DirCacheIterator(repository.readDirCache()) + formatter.format(headTree, freshIndexTree) } } outputStream.toString() From 83979dbee5a6efbabe6052d92a00c53e01b36bbc Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Fri, 6 Mar 2026 12:43:10 +0100 Subject: [PATCH 07/10] refactor(ADFA-2789): Update imports --- app/src/main/java/com/itsaky/androidide/di/AppModule.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/di/AppModule.kt b/app/src/main/java/com/itsaky/androidide/di/AppModule.kt index 39e66563fd..8e2f15ff39 100644 --- a/app/src/main/java/com/itsaky/androidide/di/AppModule.kt +++ b/app/src/main/java/com/itsaky/androidide/di/AppModule.kt @@ -5,8 +5,9 @@ import com.itsaky.androidide.agent.GeminiMacroProcessor import com.itsaky.androidide.agent.viewmodel.ChatViewModel import com.itsaky.androidide.analytics.AnalyticsManager import com.itsaky.androidide.analytics.IAnalyticsManager -import org.koin.androidx.viewmodel.dsl.viewModel +import com.itsaky.androidide.viewmodel.GitBottomSheetViewModel import org.koin.dsl.module +import org.koin.core.module.dsl.viewModel val coreModule = module { @@ -20,6 +21,6 @@ val coreModule = ChatViewModel() } viewModel { - com.itsaky.androidide.viewmodel.GitBottomSheetViewModel() + GitBottomSheetViewModel() } } From 3ecca8ee816f4729cc9456452be0eb09e0a93365 Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Fri, 6 Mar 2026 13:20:25 +0100 Subject: [PATCH 08/10] fix(ADFA-2879): Minor fixes --- .../fragments/git/GitDiffViewerDialog.kt | 57 ++++++++++--------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/fragments/git/GitDiffViewerDialog.kt b/app/src/main/java/com/itsaky/androidide/fragments/git/GitDiffViewerDialog.kt index 2d14ff7c74..197a594527 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/git/GitDiffViewerDialog.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/git/GitDiffViewerDialog.kt @@ -3,21 +3,23 @@ package com.itsaky.androidide.fragments.git import android.graphics.Canvas import android.graphics.Paint import android.os.Bundle -import android.text.Spannable import android.text.SpannableStringBuilder +import android.text.Spanned.SPAN_EXCLUSIVE_EXCLUSIVE import android.text.style.ForegroundColorSpan import android.text.style.LineBackgroundSpan import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.content.ContextCompat +import androidx.core.content.ContextCompat.getColor import androidx.fragment.app.DialogFragment import androidx.fragment.app.activityViewModels import androidx.lifecycle.lifecycleScope import com.itsaky.androidide.R import com.itsaky.androidide.databinding.DialogGitDiffBinding import com.itsaky.androidide.viewmodel.GitBottomSheetViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import java.io.File class GitDiffViewerDialog : DialogFragment() { @@ -26,9 +28,6 @@ class GitDiffViewerDialog : DialogFragment() { private var filePath: String = "" - private var _binding: DialogGitDiffBinding? = null - private val binding get() = _binding!! - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setStyle(STYLE_NORMAL, R.style.Theme_AndroidIDE) @@ -40,17 +39,18 @@ class GitDiffViewerDialog : DialogFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { - _binding = DialogGitDiffBinding.inflate(inflater, container, false) - return binding.root + return inflater.inflate(R.layout.dialog_git_diff, container, false) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + val binding = DialogGitDiffBinding.bind(view) + binding.diffTitle.text = filePath binding.diffText.text = getString(R.string.diff_loading) - viewLifecycleOwner.lifecycleScope.launch(kotlinx.coroutines.Dispatchers.IO) { + viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { val repo = viewModel.currentRepository val diff = if (repo != null && repo.rootDir.exists()) { val file = File(repo.rootDir, filePath) @@ -59,10 +59,8 @@ class GitDiffViewerDialog : DialogFragment() { null } ?: getString(R.string.unable_to_load_diff) - kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Main) { - if (_binding != null) { - binding.diffText.text = applyDiffFormatting(diff) - } + withContext(Dispatchers.Main) { + binding.diffText.text = applyDiffFormatting(diff) } } @@ -71,11 +69,6 @@ class GitDiffViewerDialog : DialogFragment() { } } - override fun onDestroyView() { - super.onDestroyView() - _binding = null - } - private fun applyDiffFormatting(diff: String): SpannableStringBuilder { val builder = SpannableStringBuilder() val lines = diff.split("\n") @@ -85,11 +78,11 @@ class GitDiffViewerDialog : DialogFragment() { val startIndex = if (firstChunkIndex != -1) firstChunkIndex else 0 val context = requireContext() - val colorAdd = ContextCompat.getColor(context, R.color.git_diff_add_text) - val bgAdd = ContextCompat.getColor(context, R.color.git_diff_add_bg) - val colorDel = ContextCompat.getColor(context, R.color.git_diff_del_text) - val bgDel = ContextCompat.getColor(context, R.color.git_diff_del_bg) - val colorHeader = ContextCompat.getColor(context, R.color.git_diff_header_text) + val colorAdd = getColor(context, R.color.git_diff_add_text) + val bgAdd = getColor(context, R.color.git_diff_add_bg) + val colorDel = getColor(context, R.color.git_diff_del_text) + val bgDel = getColor(context, R.color.git_diff_del_bg) + val colorHeader = getColor(context, R.color.git_diff_header_text) for (i in startIndex until lines.size) { val line = lines[i] @@ -100,22 +93,32 @@ class GitDiffViewerDialog : DialogFragment() { builder.append(formattedLine).append("\n") val endIdx = builder.length - builder.setSpan(ForegroundColorSpan(colorAdd), startIdx, endIdx, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - builder.setSpan(FullWidthBackgroundColorSpan(bgAdd), startIdx, endIdx, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + builder.setSpan(ForegroundColorSpan(colorAdd), startIdx, endIdx, + SPAN_EXCLUSIVE_EXCLUSIVE + ) + builder.setSpan(FullWidthBackgroundColorSpan(bgAdd), startIdx, endIdx, + SPAN_EXCLUSIVE_EXCLUSIVE + ) } line.startsWith("-") && !line.startsWith("---") -> { val formattedLine = line.replaceFirst("-", "- ") builder.append(formattedLine).append("\n") val endIdx = builder.length - builder.setSpan(ForegroundColorSpan(colorDel), startIdx, endIdx, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - builder.setSpan(FullWidthBackgroundColorSpan(bgDel), startIdx, endIdx, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + builder.setSpan(ForegroundColorSpan(colorDel), startIdx, endIdx, + SPAN_EXCLUSIVE_EXCLUSIVE + ) + builder.setSpan(FullWidthBackgroundColorSpan(bgDel), startIdx, endIdx, + SPAN_EXCLUSIVE_EXCLUSIVE + ) } line.startsWith("@@") -> { builder.append(line).append("\n") val endIdx = builder.length - builder.setSpan(ForegroundColorSpan(colorHeader), startIdx, endIdx, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + builder.setSpan(ForegroundColorSpan(colorHeader), startIdx, endIdx, + SPAN_EXCLUSIVE_EXCLUSIVE + ) } else -> { val formattedLine = if (line.startsWith(" ")) " $line" else line From 08d7e25b89dba89b3b431f5ca92559a0e8d06e52 Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Fri, 6 Mar 2026 18:06:21 +0100 Subject: [PATCH 09/10] feat(ADFA-2879): Refresh git status when files are deleted or renamed --- .../viewmodel/GitBottomSheetViewModel.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt b/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt index cb6f5eb412..f9b573e677 100644 --- a/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt +++ b/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt @@ -15,6 +15,9 @@ import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode +import com.itsaky.androidide.eventbus.events.file.FileCreationEvent +import com.itsaky.androidide.eventbus.events.file.FileDeletionEvent +import com.itsaky.androidide.eventbus.events.file.FileRenameEvent import java.io.File class GitBottomSheetViewModel : ViewModel() { @@ -65,4 +68,19 @@ class GitBottomSheetViewModel : ViewModel() { fun onProjectFilesChanged(event: ListProjectFilesRequestEvent) { refreshStatus() } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onFileCreated(event: FileCreationEvent) { + refreshStatus() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onFileDeleted(event: FileDeletionEvent) { + refreshStatus() + } + + @Subscribe(threadMode = ThreadMode.MAIN) + fun onFileRenamed(event: FileRenameEvent) { + refreshStatus() + } } From 97a0e0c61e4cda07c61c86730868a20efcbe302d Mon Sep 17 00:00:00 2001 From: Oluwadara Abijo Date: Mon, 9 Mar 2026 18:00:50 +0100 Subject: [PATCH 10/10] feat(ADFA-2879): Handle exceptions when initialising repo --- .../viewmodel/GitBottomSheetViewModel.kt | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt b/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt index f9b573e677..479aff4879 100644 --- a/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt +++ b/app/src/main/java/com/itsaky/androidide/viewmodel/GitBottomSheetViewModel.kt @@ -3,6 +3,9 @@ package com.itsaky.androidide.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.itsaky.androidide.eventbus.events.editor.DocumentSaveEvent +import com.itsaky.androidide.eventbus.events.file.FileCreationEvent +import com.itsaky.androidide.eventbus.events.file.FileDeletionEvent +import com.itsaky.androidide.eventbus.events.file.FileRenameEvent import com.itsaky.androidide.events.ListProjectFilesRequestEvent import com.itsaky.androidide.git.core.GitRepository import com.itsaky.androidide.git.core.GitRepositoryManager @@ -15,13 +18,13 @@ import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import com.itsaky.androidide.eventbus.events.file.FileCreationEvent -import com.itsaky.androidide.eventbus.events.file.FileDeletionEvent -import com.itsaky.androidide.eventbus.events.file.FileRenameEvent +import org.slf4j.LoggerFactory import java.io.File class GitBottomSheetViewModel : ViewModel() { + private val log = LoggerFactory.getLogger(GitBottomSheetViewModel::class.java) + private val _gitStatus = MutableStateFlow(GitStatus.EMPTY) val gitStatus: StateFlow = _gitStatus.asStateFlow() @@ -41,9 +44,14 @@ class GitBottomSheetViewModel : ViewModel() { private fun initializeRepository() { viewModelScope.launch { - val projectDir = File(IProjectManager.getInstance().projectDirPath) - currentRepository = GitRepositoryManager.openRepository(projectDir) - refreshStatus() + try { + val projectDir = File(IProjectManager.getInstance().projectDirPath) + currentRepository = GitRepositoryManager.openRepository(projectDir) + refreshStatus() + } catch (e: Exception) { + log.error("Failed to initialize repository", e) + _gitStatus.value = GitStatus.EMPTY + } } } @@ -52,9 +60,16 @@ class GitBottomSheetViewModel : ViewModel() { */ fun refreshStatus() { viewModelScope.launch { - currentRepository?.let { repo -> - val status = repo.getStatus() - _gitStatus.value = status + try { + currentRepository?.let { repo -> + val status = repo.getStatus() + _gitStatus.value = status + } ?: run { + _gitStatus.value = GitStatus.EMPTY + } + } catch (e: Exception) { + log.error("Failed to refresh git status", e) + _gitStatus.value = GitStatus.EMPTY } } }