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 @@ -1091,18 +1091,24 @@ open class EditorHandlerActivity :
@Subscribe(threadMode = ThreadMode.MAIN)
fun onDocumentChange(event: DocumentChangeEvent) {
if (contentOrNull == null) return
editorViewModel.areFilesModified = true

val fileIndex = findIndexOfEditorByFile(event.file.toFile())
if (fileIndex == -1) return

// The editor recomputes its modified flag on every content change, so a change may
// have returned the file to its saved state (e.g. the user undid all their edits).
val isModified = getEditorAtIndex(fileIndex)?.isModified ?: false
editorViewModel.areFilesModified = hasUnsavedFiles()

val tabPosition = getTabPositionForFileIndex(fileIndex)
if (tabPosition < 0) return

val tab = content.tabs.getTabAt(tabPosition) ?: return
if (tab.text?.startsWith('*') == true) return
val hasIndicator = tab.text?.startsWith('*') == true
if (isModified == hasIndicator) return

tab.text = "*${tab.text}"
val baseName = tab.text?.removePrefix("*") ?: return
tab.text = if (isModified) "*$baseName" else baseName
}

@Subscribe(threadMode = ThreadMode.MAIN)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,13 @@ constructor(
private var fileVersion = 0
internal var isModified = false

// Length and content hash of the content the last time the file was loaded or saved.
// Used to detect when edits (e.g. undoing every change) return the content to its
// saved state, so the modified indicator can be cleared. The length is compared first
// as an O(1) guard so the content hash is only computed when the lengths match.
private var savedContentLength = 0
private var savedContentHash = 0L

private val selectionChangeHandler = Handler(Looper.getMainLooper())
private var selectionChangeRunner: Runnable? =
Runnable {
Expand Down Expand Up @@ -661,9 +668,13 @@ constructor(

/**
* Mark this editor as NOT modified.
*
* Snapshots the current content so that later edits which return the content to this
* state (for example, undoing every change) can clear the modified flag again.
*/
open fun markUnmodified() {
this.isModified = false
snapshotSavedContent()
}

/**
Expand All @@ -673,6 +684,33 @@ constructor(
this.isModified = true
}

/**
* Recomputes [isModified] by comparing the current content against the snapshot captured
* the last time the file was loaded or saved. The content length is
* checked first as a cheap guard so the full content hash is only computed when the lengths
* match - i.e. when the edits may have restored the saved state.
*/
private fun refreshModifiedState() {
val content = text
isModified = content.length != savedContentLength ||
computeContentHash(content) != savedContentHash
}

private fun snapshotSavedContent() {
val content = text
savedContentLength = content.length
savedContentHash = computeContentHash(content)
}

private fun computeContentHash(content: CharSequence): Long {
var hash = -3750763034362895579L // FNV_offset_basis

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.

Could you extract these values into constants and document them?

for (i in 0 until content.length) {
hash = hash xor content[i].code.toLong()
hash *= 1099511628211L // FNV_prime
}
return hash
}

/**
* Notify the language server that the file in this editor is about to be closed.
*/
Expand Down Expand Up @@ -890,7 +928,7 @@ constructor(
return@subscribeEvent
}

markModified()
refreshModifiedState()
file ?: return@subscribeEvent

editorScope.launch {
Expand Down
Loading