Skip to content

Commit eb169fa

Browse files
authored
feat: Offload file change detection to background thread (#1802)
1 parent 6750d9c commit eb169fa

File tree

4 files changed

+223
-41
lines changed

4 files changed

+223
-41
lines changed

bun.lock

Lines changed: 8 additions & 32 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/editorFile.js

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -610,26 +610,43 @@ export default class EditorFile {
610610
}
611611

612612
const protocol = Url.getProtocol(this.#uri);
613-
let fs;
613+
const text = this.session.getValue();
614+
614615
if (/s?ftp:/.test(protocol)) {
615616
// if file is a ftp or sftp file, get file content from cached file.
616617
// remove ':' from protocol because cache file of remote files are
617618
// stored as ftp102525465N i.e. protocol + id
619+
// Cache files are local file:// URIs, so native file reading works
618620
const cacheFilename = protocol.slice(0, -1) + this.id;
619-
const cacheFile = Url.join(CACHE_STORAGE, cacheFilename);
620-
fs = fsOperation(cacheFile);
621-
} else {
622-
fs = fsOperation(this.uri);
621+
const cacheFileUri = Url.join(CACHE_STORAGE, cacheFilename);
622+
623+
try {
624+
return await system.compareFileText(cacheFileUri, this.encoding, text);
625+
} catch (error) {
626+
console.error("Native compareFileText failed:", error);
627+
return false;
628+
}
629+
}
630+
631+
if (/^(file|content):/.test(protocol)) {
632+
// file:// and content:// URIs can be handled by native Android code
633+
// Native reads file AND compares in background thread
634+
try {
635+
return await system.compareFileText(this.uri, this.encoding, text);
636+
} catch (error) {
637+
console.error("Native compareFileText failed:", error);
638+
return false;
639+
}
623640
}
624641

625642
try {
643+
const fs = fsOperation(this.uri);
626644
const oldText = await fs.readFile(this.encoding);
627-
const text = this.session.getValue();
628645

629-
if (oldText.length !== text.length) return true;
630-
return oldText !== text;
646+
// Offload string comparison to background thread
647+
return await system.compareTexts(oldText, text);
631648
} catch (error) {
632-
window.log("error", error);
649+
console.error(error);
633650
return false;
634651
}
635652
}

src/plugins/system/android/com/foxdebug/system/System.java

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@
7878
import java.io.InputStream;
7979
import java.io.OutputStream;
8080
import java.io.IOException;
81+
import java.io.BufferedReader;
82+
import java.io.InputStreamReader;
8183

8284
import android.os.Build;
8385
import android.os.Environment;
@@ -169,6 +171,8 @@ public boolean execute(
169171
case "decode":
170172
case "encode":
171173
case "copyToUri":
174+
case "compare-file-text":
175+
case "compare-texts":
172176
break;
173177
case "get-configuration":
174178
getConfiguration(callbackContext);
@@ -505,6 +509,12 @@ public void run() {
505509
case "encode":
506510
encode(arg1, arg2, callbackContext);
507511
break;
512+
case "compare-file-text":
513+
compareFileText(arg1, arg2, arg3, callbackContext);
514+
break;
515+
case "compare-texts":
516+
compareTexts(arg1, arg2, callbackContext);
517+
break;
508518
default:
509519
break;
510520
}
@@ -616,6 +626,146 @@ private void encode(
616626
}
617627
}
618628

629+
/**
630+
* Compares file content with provided text.
631+
* This method runs in a background thread to avoid blocking the UI.
632+
*
633+
* @param fileUri The URI of the file to read (file:// or content://)
634+
* @param encoding The character encoding to use when reading the file
635+
* @param currentText The text to compare against the file content
636+
* @param callback Returns 1 if texts are different, 0 if same
637+
*/
638+
private void compareFileText(
639+
String fileUri,
640+
String encoding,
641+
String currentText,
642+
CallbackContext callback
643+
) {
644+
try {
645+
if (fileUri == null || fileUri.isEmpty()) {
646+
callback.error("File URI is required");
647+
return;
648+
}
649+
650+
if (encoding == null || encoding.isEmpty()) {
651+
encoding = "UTF-8";
652+
}
653+
654+
if (!Charset.isSupported(encoding)) {
655+
callback.error("Charset not supported: " + encoding);
656+
return;
657+
}
658+
659+
Uri uri = Uri.parse(fileUri);
660+
Charset charset = Charset.forName(encoding);
661+
String fileContent;
662+
663+
// Handle file:// URIs
664+
if ("file".equalsIgnoreCase(uri.getScheme())) {
665+
File file = new File(uri.getPath());
666+
667+
// Validate file
668+
if (!file.exists()) {
669+
callback.error("File does not exist");
670+
return;
671+
}
672+
if (!file.isFile()) {
673+
callback.error("Path is not a file");
674+
return;
675+
}
676+
if (!file.canRead()) {
677+
callback.error("File is not readable");
678+
return;
679+
}
680+
681+
Path path = file.toPath();
682+
fileContent = new String(Files.readAllBytes(path), charset);
683+
684+
} else {
685+
// Handle content:// URIs
686+
InputStream inputStream = null;
687+
try {
688+
inputStream = context.getContentResolver().openInputStream(uri);
689+
690+
if (inputStream == null) {
691+
callback.error("Cannot open file");
692+
return;
693+
}
694+
695+
StringBuilder sb = new StringBuilder();
696+
try (BufferedReader reader = new BufferedReader(
697+
new InputStreamReader(inputStream, charset))) {
698+
char[] buffer = new char[8192];
699+
int charsRead;
700+
while ((charsRead = reader.read(buffer)) != -1) {
701+
sb.append(buffer, 0, charsRead);
702+
}
703+
}
704+
fileContent = sb.toString();
705+
706+
} finally {
707+
if (inputStream != null) {
708+
try {
709+
inputStream.close();
710+
} catch (IOException ignored) {}
711+
}
712+
}
713+
}
714+
715+
// check length first
716+
if (fileContent.length() != currentText.length()) {
717+
callback.success(1); // Changed
718+
return;
719+
}
720+
721+
// Full comparison
722+
if (fileContent.equals(currentText)) {
723+
callback.success(0); // Not changed
724+
} else {
725+
callback.success(1); // Changed
726+
}
727+
728+
} catch (Exception e) {
729+
callback.error(e.toString());
730+
}
731+
}
732+
733+
/**
734+
* Compares two text strings.
735+
* This method runs in a background thread to avoid blocking the UI
736+
* for large string comparisons.
737+
*
738+
* @param text1 First text to compare
739+
* @param text2 Second text to compare
740+
* @param callback Returns 1 if texts are different, 0 if same
741+
*/
742+
private void compareTexts(
743+
String text1,
744+
String text2,
745+
CallbackContext callback
746+
) {
747+
try {
748+
if (text1 == null) text1 = "";
749+
if (text2 == null) text2 = "";
750+
751+
// check length first
752+
if (text1.length() != text2.length()) {
753+
callback.success(1); // Changed
754+
return;
755+
}
756+
757+
// Full comparison
758+
if (text1.equals(text2)) {
759+
callback.success(0); // Not changed
760+
} else {
761+
callback.success(1); // Changed
762+
}
763+
764+
} catch (Exception e) {
765+
callback.error(e.toString());
766+
}
767+
}
768+
619769
private void getAvailableEncodings(CallbackContext callback) {
620770
try {
621771
Map < String, Charset > charsets = Charset.availableCharsets();

src/plugins/system/www/plugin.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,5 +155,44 @@ module.exports = {
155155
},
156156
getGlobalSetting: function (key, onSuccess, onFail) {
157157
cordova.exec(onSuccess, onFail, 'System', 'get-global-setting', [key]);
158+
},
159+
/**
160+
* Compare file content with provided text in a background thread.
161+
* @param {string} fileUri - The URI of the file to read
162+
* @param {string} encoding - The character encoding to use
163+
* @param {string} currentText - The text to compare against
164+
* @returns {Promise<boolean>} - Resolves to true if content differs, false if same
165+
*/
166+
compareFileText: function (fileUri, encoding, currentText) {
167+
return new Promise((resolve, reject) => {
168+
cordova.exec(
169+
function(result) {
170+
resolve(result === 1);
171+
},
172+
reject,
173+
'System',
174+
'compare-file-text',
175+
[fileUri, encoding, currentText]
176+
);
177+
});
178+
},
179+
/**
180+
* Compare two text strings in a background thread.
181+
* @param {string} text1 - First text to compare
182+
* @param {string} text2 - Second text to compare
183+
* @returns {Promise<boolean>} - Resolves to true if texts differ, false if same
184+
*/
185+
compareTexts: function (text1, text2) {
186+
return new Promise((resolve, reject) => {
187+
cordova.exec(
188+
function(result) {
189+
resolve(result === 1);
190+
},
191+
reject,
192+
'System',
193+
'compare-texts',
194+
[text1, text2]
195+
);
196+
});
158197
}
159198
};

0 commit comments

Comments
 (0)