From 6765224c18535a6db60c6d6e0e1522b1060ab13e Mon Sep 17 00:00:00 2001 From: Kaarel Kaljurand Date: Wed, 12 Apr 2023 23:59:51 +0200 Subject: [PATCH 1/4] Add an op that POSTs JSON --- app/build.gradle | 1 + .../speechutils/editor/CommandEditor.java | 23 +++++++ .../editor/CommandEditorManager.java | 4 ++ .../speechutils/editor/FunctionExpander.kt | 66 +++++++++++++++++++ .../editor/InputConnectionCommandEditor.java | 66 +++++++++++-------- .../android/speechutils/utils/HttpUtils.java | 29 ++++++++ .../android/speechutils/utils/JsonUtils.java | 2 +- 7 files changed, 162 insertions(+), 29 deletions(-) create mode 100644 app/src/main/java/ee/ioc/phon/android/speechutils/editor/FunctionExpander.kt diff --git a/app/build.gradle b/app/build.gradle index 8a30e54..153e737 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,6 +13,7 @@ dependencies { implementation 'androidx.annotation:annotation:1.6.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.github.curious-odd-man:rgxgen:1.4' + implementation 'com.jayway.jsonpath:json-path:2.8.0' } android { diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandEditor.java b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandEditor.java index 2f7468a..e372859 100644 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandEditor.java +++ b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandEditor.java @@ -184,6 +184,29 @@ public interface CommandEditor { // E.g. url == "http://api.mathjs.org/v4/?expr="; arg == "@sel()+1" Op getUrl(String url, String arg); + /** + * Replace cursor with the result of the given HTTP-query. + * The query is specified as a string that contains a JSON object. + * The result is also expected to be a JSON object, on which a JSON Path + * query will be applied to extract a string that replaces the current selection. + * { + * "url": "https://api.example.org/v1/edits", + * "method": "POST", + * "body": { + * "model": "text-davinci-edit-001", + * "input": "@sel()", + * "instruction": "Fix the spelling mistakes" + * }, + * "header": { + * "Authorization": "Bearer API_KEY", + * "Content-Type": "application/json", + * "User-Agent": "K6nele/httpJson/Edits" + * }, + * "jsonpath": "$.choices[0].text" + * } + */ + Op httpJson(String json); + // Commands that are not exposed to the end-user in CommandEditorManager CommandEditorResult commitFinalResult(String text); diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandEditorManager.java b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandEditorManager.java index e1fe5c6..451c714 100644 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandEditorManager.java +++ b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandEditorManager.java @@ -48,6 +48,8 @@ public class CommandEditorManager { public static final String ACTIVITY = "activity"; public static final String GET_URL = "getUrl"; + public static final String HTTP_JSON = "httpJson"; + public static final Map EDITOR_COMMANDS; static { @@ -175,6 +177,8 @@ public class CommandEditorManager { return ce.getUrl(urlPrefix, urlArg); }); + aMap.put(HTTP_JSON, (ce, args) -> ce.httpJson(getArgString(args, 0, null))); + EDITOR_COMMANDS = Collections.unmodifiableMap(aMap); } diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/FunctionExpander.kt b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/FunctionExpander.kt new file mode 100644 index 0000000..01987f0 --- /dev/null +++ b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/FunctionExpander.kt @@ -0,0 +1,66 @@ +package ee.ioc.phon.android.speechutils.editor + +import android.os.Build +import android.view.inputmethod.InputConnection +import java.text.DateFormat +import java.text.SimpleDateFormat +import java.util.* +import java.util.regex.Matcher +import java.util.regex.Pattern + +private val F_SELECTION = Pattern.compile("@sel\\(\\)") +private val F_TIMESTAMP = Pattern.compile("@timestamp\\(([^,]+), *([^,]+)\\)") + +fun expandFuns(input: String, vararg editFuns: EditFunction): String { + val resultString = StringBuffer(input) + editFuns.forEach { + val regexMatcher = it.pattern.matcher(resultString) + while (regexMatcher.find()) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + regexMatcher.appendReplacement(resultString, it.apply(regexMatcher)) + } + } + regexMatcher.appendTail(resultString) + } + + return resultString.toString() +} + +abstract class EditFunction { + abstract val pattern: Pattern + abstract fun apply(m: Matcher): String +} + +class Timestamp : EditFunction() { + override val pattern: Pattern = F_TIMESTAMP + + private val currentTime: Date by lazy { + Calendar.getInstance().time + } + + override fun apply(m: Matcher): String { + val df: DateFormat = SimpleDateFormat(m.group(1), Locale(m.group(2))) + return df.format(currentTime) + } +} + +class Sel(ic: InputConnection) : EditFunction() { + override val pattern: Pattern = F_SELECTION + + private val selectedText: String by lazy { + val cs: CharSequence = ic.getSelectedText(0) + cs.toString() + } + + override fun apply(m: Matcher): String { + return selectedText + } +} + +class SelEvaluated(private val selectedText: String) : EditFunction() { + override val pattern: Pattern = F_SELECTION + + override fun apply(m: Matcher): String { + return this.selectedText + } +} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditor.java b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditor.java index 13db78c..c004162 100644 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditor.java +++ b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditor.java @@ -2,6 +2,7 @@ import static android.os.Build.VERSION_CODES; +import android.content.ActivityNotFoundException; import android.content.Context; import android.os.AsyncTask; import android.os.Build.VERSION; @@ -18,16 +19,11 @@ import org.json.JSONException; import java.io.IOException; -import java.text.DateFormat; -import java.text.SimpleDateFormat; import java.util.ArrayDeque; import java.util.ArrayList; -import java.util.Calendar; import java.util.Collection; -import java.util.Date; import java.util.Deque; import java.util.List; -import java.util.Locale; import java.util.NoSuchElementException; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -52,7 +48,6 @@ public class InputConnectionCommandEditor implements CommandEditor { // Token optionally preceded by whitespace private static final Pattern WHITESPACE_AND_TOKEN = Pattern.compile("\\s*\\w+"); private static final String F_SELECTION = "@sel()"; - private static final Pattern F_TIMESTAMP = Pattern.compile("@timestamp\\(([^,]+), *([^,]+)\\)"); private Context mContext; @@ -253,6 +248,8 @@ public Op run() { Log.i("startActivity: JSON: " + e.getMessage()); } catch (SecurityException e) { Log.i("startActivity: Security: " + e.getMessage()); + } catch (ActivityNotFoundException e) { + Log.i("startActivity: NotFound: " + e.getMessage()); } return undo; } @@ -292,6 +289,32 @@ protected void onPostExecute(String result) { }; } + @Override + public Op httpJson(final String json) { + return new Op("httpJson") { + @Override + public Op run() { + new AsyncTask() { + + @Override + protected String doInBackground(String... str) { + try { + return HttpUtils.fetchUrl(str[0]); + } catch (IOException | JSONException e) { + return "[ERROR: Unable to query " + str[0] + ": " + e.getLocalizedMessage() + "]"; + } + } + + @Override + protected void onPostExecute(String result) { + runOp(replaceSel(result)); + } + }.execute(expandFunsAll(json)); + return Op.NO_OP; + } + }; + } + @Override public Op keyUp() { return new Op("goUp") { @@ -778,27 +801,11 @@ public Op replaceSel(final String str) { return replaceSel(str, null); } - /** - * TODO: generalize to any functions - */ - private String expandFuns(String line) { - Matcher m = F_TIMESTAMP.matcher(line); - String newLine = ""; - int pos = 0; - Date currentTime = null; - while (m.find()) { - if (currentTime == null) { - currentTime = Calendar.getInstance().getTime(); - } - newLine += line.substring(pos, m.start()); - DateFormat df = new SimpleDateFormat(m.group(1), new Locale(m.group(2))); - newLine += df.format(currentTime); - pos = m.end(); - } - if (pos == 0) { - return line; - } - return newLine + line.substring(pos); + private String expandFunsAll(String line) { + return FunctionExpanderKt.expandFuns(line, + new Sel(getInputConnection()), + new Timestamp() + ); } /** @@ -818,7 +825,10 @@ public Op run() { if (str == null || str.isEmpty()) { newText = ""; } else { - newText = expandFuns(str.replace(F_SELECTION, selectedText)); + newText = FunctionExpanderKt.expandFuns(str, + new SelEvaluated(selectedText), + new Timestamp() + ); } Op op = null; if (regex != null) { diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/utils/HttpUtils.java b/app/src/main/java/ee/ioc/phon/android/speechutils/utils/HttpUtils.java index a9e5ba6..71947fb 100644 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/utils/HttpUtils.java +++ b/app/src/main/java/ee/ioc/phon/android/speechutils/utils/HttpUtils.java @@ -1,5 +1,10 @@ package ee.ioc.phon.android.speechutils.utils; +import com.jayway.jsonpath.JsonPath; + +import org.json.JSONException; +import org.json.JSONObject; + import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -10,6 +15,8 @@ import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Iterator; import java.util.Map; import ee.ioc.phon.android.speechutils.Log; @@ -32,6 +39,28 @@ public static String fetchUrl(String myurl, String method, String body) throws I return fetchUrl(myurl, method, body, null); } + public static String fetchUrl(String jsonAsStr) throws IOException, JSONException { + JSONObject json = JsonUtils.parseJson(jsonAsStr); + String url = json.optString("url"); + String jsonpath = json.optString("jsonpath"); + String method = json.optString("method", "GET"); + JSONObject header = json.optJSONObject("header"); + Map properties = new HashMap<>(); + if (header != null) { + Iterator iter = header.keys(); + while (iter.hasNext()) { + String key = iter.next(); + properties.put(key, header.optString(key, "")); + } + } + JSONObject body = json.optJSONObject("body"); + String result = fetchUrl(url, method, body == null ? null : body.toString(), properties); + if (jsonpath.isEmpty()) { + return result; + } + return JsonPath.read(result, jsonpath); + } + public static String fetchUrl(String myurl, String method, String body, Map properties) throws IOException { byte[] outputInBytes = null; diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/utils/JsonUtils.java b/app/src/main/java/ee/ioc/phon/android/speechutils/utils/JsonUtils.java index 274708d..a6e6c57 100644 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/utils/JsonUtils.java +++ b/app/src/main/java/ee/ioc/phon/android/speechutils/utils/JsonUtils.java @@ -26,7 +26,7 @@ private JsonUtils() { * @return JSONObject * @throws JSONException if parsing fails */ - private static JSONObject parseJson(CharSequence chars) throws JSONException { + public static JSONObject parseJson(CharSequence chars) throws JSONException { if (chars == null) { throw new JSONException("input is NULL"); } From 8884d606bcc34bfdd0ac35af52f50c87d75a9891 Mon Sep 17 00:00:00 2001 From: Kaarel Kaljurand Date: Tue, 18 Apr 2023 00:06:05 +0200 Subject: [PATCH 2/4] Some updates --- app/build.gradle | 2 + .../speechutils/editor/FunctionExpander.kt | 61 +++++++++++++++++-- .../editor/InputConnectionCommandEditor.java | 15 +---- 3 files changed, 62 insertions(+), 16 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 153e737..4641be8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' dependencies { // Required -- JUnit 4 framework @@ -9,6 +10,7 @@ dependencies { // Optional -- Hamcrest library androidTestImplementation 'org.hamcrest:hamcrest-library:1.3' + implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.10' implementation 'commons-io:commons-io:2.5' implementation 'androidx.annotation:annotation:1.6.0' implementation 'androidx.appcompat:appcompat:1.6.1' diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/FunctionExpander.kt b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/FunctionExpander.kt index 01987f0..392ee61 100644 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/FunctionExpander.kt +++ b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/FunctionExpander.kt @@ -1,6 +1,8 @@ package ee.ioc.phon.android.speechutils.editor import android.os.Build +import android.view.inputmethod.ExtractedText +import android.view.inputmethod.ExtractedTextRequest import android.view.inputmethod.InputConnection import java.text.DateFormat import java.text.SimpleDateFormat @@ -8,14 +10,14 @@ import java.util.* import java.util.regex.Matcher import java.util.regex.Pattern -private val F_SELECTION = Pattern.compile("@sel\\(\\)") -private val F_TIMESTAMP = Pattern.compile("@timestamp\\(([^,]+), *([^,]+)\\)") +private val F_SELECTION = Pattern.compile("""@sel\(\)""") fun expandFuns(input: String, vararg editFuns: EditFunction): String { val resultString = StringBuffer(input) editFuns.forEach { val regexMatcher = it.pattern.matcher(resultString) while (regexMatcher.find()) { + // TODO: lift required API to N everywhere if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { regexMatcher.appendReplacement(resultString, it.apply(regexMatcher)) } @@ -26,13 +28,51 @@ fun expandFuns(input: String, vararg editFuns: EditFunction): String { return resultString.toString() } +fun expandFunsAll(line: String, ic: InputConnection): String { + return expandFuns( + line, + Sel(ic), + Text(ic), + Expr(), + Timestamp() + ) +} + +fun expandFuns2(line: String, selectedText: String, ic: InputConnection): String { + return expandFuns( + line, + SelEvaluated(selectedText), + Text(ic), + Expr(), + Timestamp() + ) +} + abstract class EditFunction { abstract val pattern: Pattern abstract fun apply(m: Matcher): String } +class Expr : EditFunction() { + override val pattern: Pattern = Pattern.compile("""@expr\((\d+) ?([+/*-]) ?(\d+)\)""") + + private fun applyToInt(sign: String, a: Int, b: Int): Int { + return when (sign) { + "+" -> a + b + "-" -> a - b + "*" -> a * b + "/" -> a / b + else -> 0 + } + } + + override fun apply(m: Matcher): String { + return applyToInt(m.group(1), m.group(1).toInt(), m.group(3).toInt()).toString() + } +} + class Timestamp : EditFunction() { - override val pattern: Pattern = F_TIMESTAMP + override val pattern: Pattern = Pattern.compile("""@timestamp\(([^,]+), *([^,]+)\)""") private val currentTime: Date by lazy { Calendar.getInstance().time @@ -44,7 +84,7 @@ class Timestamp : EditFunction() { } } -class Sel(ic: InputConnection) : EditFunction() { +public class Sel(ic: InputConnection) : EditFunction() { override val pattern: Pattern = F_SELECTION private val selectedText: String by lazy { @@ -63,4 +103,17 @@ class SelEvaluated(private val selectedText: String) : EditFunction() { override fun apply(m: Matcher): String { return this.selectedText } +} + +class Text(private val ic: InputConnection) : EditFunction() { + override val pattern: Pattern = Pattern.compile("""@text\(\)""") + + private val text: String by lazy { + val extractedText: ExtractedText = ic.getExtractedText(ExtractedTextRequest(), 0) + extractedText.text.toString() + } + + override fun apply(m: Matcher): String { + return text + } } \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditor.java b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditor.java index c004162..c783ad0 100644 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditor.java +++ b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditor.java @@ -33,6 +33,7 @@ import ee.ioc.phon.android.speechutils.utils.IntentUtils; import ee.ioc.phon.android.speechutils.utils.JsonUtils; + /** * TODO: this is work in progress * TODO: keep track of added spaces @@ -309,7 +310,7 @@ protected String doInBackground(String... str) { protected void onPostExecute(String result) { runOp(replaceSel(result)); } - }.execute(expandFunsAll(json)); + }.execute(FunctionExpanderKt.expandFunsAll(json, mInputConnection)); return Op.NO_OP; } }; @@ -801,13 +802,6 @@ public Op replaceSel(final String str) { return replaceSel(str, null); } - private String expandFunsAll(String line) { - return FunctionExpanderKt.expandFuns(line, - new Sel(getInputConnection()), - new Timestamp() - ); - } - /** * Commits texts and creates a new selection (within the commited text). * TODO: fix undo @@ -825,10 +819,7 @@ public Op run() { if (str == null || str.isEmpty()) { newText = ""; } else { - newText = FunctionExpanderKt.expandFuns(str, - new SelEvaluated(selectedText), - new Timestamp() - ); + newText = FunctionExpanderKt.expandFuns2(str, selectedText, mInputConnection); } Op op = null; if (regex != null) { From 9418c5442bd26eb6a1df8041592541ad577b9a7b Mon Sep 17 00:00:00 2001 From: Kaarel Kaljurand Date: Tue, 18 Apr 2023 23:35:16 +0200 Subject: [PATCH 3/4] More updates --- app/build.gradle | 2 +- .../InputConnectionCommandEditorTest.java | 22 +++++++++ .../speechutils/editor/FunctionExpander.kt | 45 ++++++++++++++----- .../editor/InputConnectionCommandEditor.java | 30 ++++++------- 4 files changed, 71 insertions(+), 28 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4641be8..c3f614b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,7 +22,7 @@ android { compileSdkVersion rootProject.compileSdkVersion defaultConfig { - minSdkVersion 23 + minSdkVersion 24 targetSdkVersion 33 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditorTest.java b/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditorTest.java index ff4c3b9..b71c929 100644 --- a/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditorTest.java +++ b/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditorTest.java @@ -66,6 +66,8 @@ public class InputConnectionCommandEditorTest { list2.add(new Command("replaceSelRe ([^ ]+) .+ ([^ ]+)", "", "replaceSelRe", new String[]{"$1 ([^ ]+) $2", "$1 \\$1 $2"})); list2.add(new Command("selection_quote", "", "replaceSel", new String[]{"\"@sel()\""})); list2.add(new Command("selection_double", "", "replaceSel", new String[]{"@sel()@sel()"})); + list2.add(new Command("text_double", "", "replaceSel", new String[]{"@text()"})); + list2.add(new Command("expr", "", "replaceSel", new String[]{"@expr(1+2)"})); // Hyphenates the current selection to the uttered number, adds brackets around the whole thing, // and selects the uttered number. Notice the need to escape the closing bracket and the end marking dollar sign. list2.add(new Command("selection_bracket ([0-9]+)", "", "replaceSel", new String[]{"(@sel()-$1)", "-([0-9]+)\\\\)\\$"})); @@ -1526,6 +1528,26 @@ public void test221() { assertThatTextIs("n. Chr. text"); } + @Test + public void test222() { + add("123456"); + add("text_double"); + assertThatTextIs("123456123456"); + } + + @Test + public void test223() { + add("expr"); + assertThatTextIs("3"); + } + + @Test + public void test224() { + add("2", "select 2"); + runOp(mEditor.replaceSel("@expr(@text() - @sel())")); + assertThatTextIs("0"); + } + private String getTextBeforeCursor(int n) { return mEditor.getInputConnection().getTextBeforeCursor(n, 0).toString(); } diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/FunctionExpander.kt b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/FunctionExpander.kt index 392ee61..f26bc27 100644 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/FunctionExpander.kt +++ b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/FunctionExpander.kt @@ -1,6 +1,5 @@ package ee.ioc.phon.android.speechutils.editor -import android.os.Build import android.view.inputmethod.ExtractedText import android.view.inputmethod.ExtractedTextRequest import android.view.inputmethod.InputConnection @@ -13,19 +12,18 @@ import java.util.regex.Pattern private val F_SELECTION = Pattern.compile("""@sel\(\)""") fun expandFuns(input: String, vararg editFuns: EditFunction): String { - val resultString = StringBuffer(input) + var inputNew = input editFuns.forEach { - val regexMatcher = it.pattern.matcher(resultString) + val resultString = StringBuffer() + val regexMatcher = it.pattern.matcher(inputNew) while (regexMatcher.find()) { - // TODO: lift required API to N everywhere - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - regexMatcher.appendReplacement(resultString, it.apply(regexMatcher)) - } + regexMatcher.appendReplacement(resultString, it.apply(regexMatcher)) } regexMatcher.appendTail(resultString) + inputNew = resultString.toString() } - return resultString.toString() + return inputNew } fun expandFunsAll(line: String, ic: InputConnection): String { @@ -48,6 +46,15 @@ fun expandFuns2(line: String, selectedText: String, ic: InputConnection): String ) } +/** + * Returns the current selection wrapped in regex quotes. + */ +public fun getSelectionAsRe(et: ExtractedText): CharSequence { + return if (et.selectionStart == et.selectionEnd) { + "" + } else "\\Q" + et.text.subSequence(et.selectionStart, et.selectionEnd) + "\\E" +} + abstract class EditFunction { abstract val pattern: Pattern abstract fun apply(m: Matcher): String @@ -67,7 +74,7 @@ class Expr : EditFunction() { } override fun apply(m: Matcher): String { - return applyToInt(m.group(1), m.group(1).toInt(), m.group(3).toInt()).toString() + return applyToInt(m.group(2), m.group(1).toInt(), m.group(3).toInt()).toString() } } @@ -84,12 +91,26 @@ class Timestamp : EditFunction() { } } -public class Sel(ic: InputConnection) : EditFunction() { +class Sel(ic: InputConnection) : EditFunction() { + override val pattern: Pattern = F_SELECTION + + private val selectedText: String by lazy { + val cs: CharSequence? = ic.getSelectedText(0) + cs?.toString().orEmpty() + } + + override fun apply(m: Matcher): String { + return selectedText + } +} + +class SelFromExtractedText(private val et: ExtractedText) : EditFunction() { override val pattern: Pattern = F_SELECTION private val selectedText: String by lazy { - val cs: CharSequence = ic.getSelectedText(0) - cs.toString() + if (et.selectionStart == et.selectionEnd) { + "" + } else """\Q""" + et.text.subSequence(et.selectionStart, et.selectionEnd) + """\E""" } override fun apply(m: Matcher): String { diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditor.java b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditor.java index c783ad0..4281ff7 100644 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditor.java +++ b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditor.java @@ -1,6 +1,8 @@ package ee.ioc.phon.android.speechutils.editor; import static android.os.Build.VERSION_CODES; +import static ee.ioc.phon.android.speechutils.editor.FunctionExpanderKt.expandFuns; +import static ee.ioc.phon.android.speechutils.editor.FunctionExpanderKt.getSelectionAsRe; import android.content.ActivityNotFoundException; import android.content.Context; @@ -243,7 +245,8 @@ public Op activity(final String json) { public Op run() { Op undo = null; try { - IntentUtils.startActivity(mContext, JsonUtils.createIntent(json.replace(F_SELECTION, getSelectedText()))); + String jsonExpanded = expandFuns(json, new Sel(mInputConnection)); + IntentUtils.startActivity(mContext, JsonUtils.createIntent(jsonExpanded)); undo = NO_OP; } catch (JSONException e) { Log.i("startActivity: JSON: " + e.getMessage()); @@ -263,11 +266,12 @@ public Op getUrl(final String url, final String arg) { @Override public Op run() { String selectedText = getSelectedText(); + SelEvaluated sel = new SelEvaluated(selectedText); final String url1; if (arg != null && !arg.isEmpty()) { - url1 = url.replace(F_SELECTION, selectedText) + HttpUtils.encode(arg.replace(F_SELECTION, selectedText)); + url1 = expandFuns(url, sel) + HttpUtils.encode(expandFuns(arg, sel)); } else { - url1 = url.replace(F_SELECTION, selectedText); + url1 = expandFuns(url, sel); } new AsyncTask() { @@ -650,7 +654,8 @@ public Op run() { mInputConnection.beginBatchEdit(); ExtractedText et = getExtractedText(); if (et != null) { - Pair queryResult = lastIndexOf(query.replace(F_SELECTION, getSelectedText()), et); + String queryExpanded = expandFuns(query, new Sel(mInputConnection)); + Pair queryResult = lastIndexOf(queryExpanded, et); if (queryResult.first >= 0) { undo = getOpSetSelection(queryResult.first, queryResult.first + queryResult.second.length(), et.selectionStart, et.selectionEnd).run(); } @@ -661,16 +666,6 @@ public Op run() { }; } - /** - * Returns the current selection wrapped in regex quotation. - */ - private CharSequence getSelectionAsRe(ExtractedText et) { - if (et.selectionStart == et.selectionEnd) { - return ""; - } - return "\\Q" + et.text.subSequence(et.selectionStart, et.selectionEnd) + "\\E"; - } - @Override public Op selectReBefore(final String regex) { return new Op("selectReBefore") { @@ -682,7 +677,12 @@ public Op run() { if (et != null) { CharSequence input = et.text.subSequence(0, et.selectionStart); // 0 == last match - Pair pos = matchNth(Pattern.compile(regex.replace(F_SELECTION, getSelectionAsRe(et))), input, 0); + //String regexExpanded = expandFuns(regex, new SelFromExtractedText(et)); + //String regexExpanded = expandFuns(regex, new SelEvaluated(getSelectionAsRe(et).toString())); + //SelFromExtractedText sel = new SelFromExtractedText(et); + String regexExpanded = regex.replace(F_SELECTION, getSelectionAsRe(et)); + //String regexExpanded = regex.replace(F_SELECTION, sel.apply(null)); + Pair pos = matchNth(Pattern.compile(regexExpanded), input, 0); if (pos != null) { undo = getOpSetSelection(pos.first, pos.second, et.selectionStart, et.selectionEnd).run(); } From 73fb2cbeaf3530d49e8368df4f5cd930dfed14a6 Mon Sep 17 00:00:00 2001 From: Kaarel Kaljurand Date: Thu, 27 Apr 2023 23:03:18 +0200 Subject: [PATCH 4/4] More updates --- app/build.gradle | 2 + .../editor/FunctionExpanderTest.kt | 48 +++++++ .../InputConnectionCommandEditorTest.java | 15 ++- .../speechutils/editor/FunctionExpander.kt | 121 ++++++++++++++++-- .../editor/InputConnectionCommandEditor.java | 46 ++++--- 5 files changed, 198 insertions(+), 34 deletions(-) create mode 100644 app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/FunctionExpanderTest.kt diff --git a/app/build.gradle b/app/build.gradle index c3f614b..24275d5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,6 +11,8 @@ dependencies { androidTestImplementation 'org.hamcrest:hamcrest-library:1.3' implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.10' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' implementation 'commons-io:commons-io:2.5' implementation 'androidx.annotation:annotation:1.6.0' implementation 'androidx.appcompat:appcompat:1.6.1' diff --git a/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/FunctionExpanderTest.kt b/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/FunctionExpanderTest.kt new file mode 100644 index 0000000..743d054 --- /dev/null +++ b/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/FunctionExpanderTest.kt @@ -0,0 +1,48 @@ +package ee.ioc.phon.android.speechutils.editor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import ee.ioc.phon.android.speechutils.utils.HttpUtils +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +private const val F_SEL = "@sel()" + +@RunWith(AndroidJUnit4::class) +class FunctionExpanderTest { + @Test + fun test01() { + val regex = "${F_SEL}|${F_SEL}" + val selContent = "SEL" + val gold = "SEL|SEL" + val regexExpanded1: String = regex.replace(F_SEL, selContent) + val regexExpanded2 = expandFuns(regex, SelEvaluated(selContent)) + assertTrue(regexExpanded1 == gold) + assertTrue(regexExpanded2 == gold) + } + + @Test + fun test02() { + val text = "@timestamp(G 'text', de)|@timestamp(G 'text', de)" + val gold = "n. Chr. text|n. Chr. text" + val res = expandFuns(text, Timestamp()) + assertTrue(res == gold) + } + + @Test + fun test03() { + val text = "@urlEncode(1+2)" + val gold = HttpUtils.encode("1+2") + val res = expandFuns(text, UrlEncode()) + assertTrue(res == gold) + } + + // TODO: set up internet permission + // @Test + fun testXX() { + val text = "@getUrl(https://api.mathjs.org/v4/?expr=@urlEncode(1+2))" + val gold = "3" + val res = expandFuns(text, UrlEncode(), GetUrl()) + assertTrue(res == gold) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditorTest.java b/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditorTest.java index b71c929..b56ffe5 100644 --- a/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditorTest.java +++ b/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditorTest.java @@ -1246,15 +1246,22 @@ public void test105() { } @Test - public void test106() { + public void test106a() { add("1 2 3 1 2 3 1 2 3"); + // 1 2 3 1 2 3 1 [2] 3 runOp(mEditor.selectReBefore("2")); + // 1 [2] 3 1 2 3 1 2 3 add("apply 2"); + // 1 2 3 1 [2] 3 1 2 3 add("next_sel"); + // 1 2 3 1 *[] 3 1 2 3 add("*"); assertThatTextIs("1 2 3 1 * 3 1 2 3"); + // 1 2 3 1 * 3 1 [2] 3 runOp(mEditor.selectReAfter("2", 1)); + // 1 [2] 3 1 * 3 1 2 3 add("prev_sel"); + // 1 * 3 1 * 3 1 2 3 add("*"); assertThatTextIs("1 * 3 1 * 3 1 2 3"); undo(2); @@ -1425,19 +1432,21 @@ public void test211() { } /** - * selectReBefore interprets the selection as a plain string (not as a regex + * selectReBefore interprets the selection as a plain string (not as a regex) */ @Test public void test212() { add(". 2 ."); + // Select last dot runOp(mEditor.selectReBefore("\\.")); + // Select first dot, because "\Q.\E" is matched runOp(mEditor.selectReBefore("@sel()")); add("1"); assertThatTextIs("1 2 ."); } /** - * selectReAfter interprets the selection as a plain string (not as a regex + * selectReAfter interprets the selection as a plain string (not as a regex) */ @Test public void test213() { diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/FunctionExpander.kt b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/FunctionExpander.kt index f26bc27..ee26379 100644 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/FunctionExpander.kt +++ b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/FunctionExpander.kt @@ -3,6 +3,10 @@ package ee.ioc.phon.android.speechutils.editor import android.view.inputmethod.ExtractedText import android.view.inputmethod.ExtractedTextRequest import android.view.inputmethod.InputConnection +import ee.ioc.phon.android.speechutils.utils.HttpUtils +import kotlinx.coroutines.* +import org.json.JSONException +import java.io.IOException import java.text.DateFormat import java.text.SimpleDateFormat import java.util.* @@ -31,16 +35,10 @@ fun expandFunsAll(line: String, ic: InputConnection): String { line, Sel(ic), Text(ic), - Expr(), - Timestamp() - ) -} - -fun expandFuns2(line: String, selectedText: String, ic: InputConnection): String { - return expandFuns( - line, - SelEvaluated(selectedText), - Text(ic), + Lower(), + Upper(), + UrlEncode(), + GetUrl(), Expr(), Timestamp() ) @@ -91,10 +89,42 @@ class Timestamp : EditFunction() { } } +class UrlEncode : EditFunction() { + override val pattern: Pattern = Pattern.compile("""@urlEncode\((.+?)\)""") + + override fun apply(m: Matcher): String { + return HttpUtils.encode(m.group(1)) + } +} + +class Lower : EditFunction() { + override val pattern: Pattern = Pattern.compile("""@lower\((.+?)\)""") + + override fun apply(m: Matcher): String { + return m.group(1).lowercase() + } +} + +class Upper : EditFunction() { + override val pattern: Pattern = Pattern.compile("""@upper\((.+?)\)""") + + override fun apply(m: Matcher): String { + return m.group(1).uppercase() + } +} + +class GetUrl : EditFunction() { + override val pattern: Pattern = Pattern.compile("""@getUrl\((.+?)\)""") + + override fun apply(m: Matcher): String { + return getUrlWithCatch(m.group(1)) + } +} + class Sel(ic: InputConnection) : EditFunction() { override val pattern: Pattern = F_SELECTION - private val selectedText: String by lazy { + val selectedText: String by lazy { val cs: CharSequence? = ic.getSelectedText(0) cs?.toString().orEmpty() } @@ -107,12 +137,14 @@ class Sel(ic: InputConnection) : EditFunction() { class SelFromExtractedText(private val et: ExtractedText) : EditFunction() { override val pattern: Pattern = F_SELECTION - private val selectedText: String by lazy { + private val selectedTextLazy: String by lazy { if (et.selectionStart == et.selectionEnd) { "" } else """\Q""" + et.text.subSequence(et.selectionStart, et.selectionEnd) + """\E""" } + private val selectedText = getSelectionAsRe(et).toString() + override fun apply(m: Matcher): String { return selectedText } @@ -137,4 +169,69 @@ class Text(private val ic: InputConnection) : EditFunction() { override fun apply(m: Matcher): String { return text } +} + +suspend fun doSomethingUsefulOne(): Int { + delay(1000L) // pretend we are doing something useful here + return 13 +} + +suspend fun doSomethingThree(v: Deferred): Int { + delay(1000L) // pretend we are doing something useful here, too + return v.await() + 29 +} + +// TODO: Add the functions as arguments +// Make a fun that calls HTTP and applies JSON query to its result. +suspend fun noh2() { + val one = CoroutineScope(Dispatchers.IO).async { + doSomethingUsefulOne() + } + val two = CoroutineScope(Dispatchers.IO).async { + doSomethingThree(one) + } + println("The answer is ${two.await()}") +} + +fun getUrlWithCatch(url: String): String { + return try { + HttpUtils.getUrl(url) + } catch (e: IOException) { + "[ERROR: Unable to retrieve " + url + ": " + e.localizedMessage + "]" + } +} + +// https://api.mathjs.org/v4/?expr=2*(7-3) +suspend fun mathjs(expr: String): String { + val url = "https://api.mathjs.org/v4/?expr=" + HttpUtils.encode(expr) + val res = CoroutineScope(Dispatchers.IO).async { + getUrlWithCatch(url) + } + return res.await() +} + +suspend fun httpJson(editor: InputConnectionCommandEditor, json: String): Op { + return object : Op("httpJson") { + override fun run(): Op { + val jsonExpanded = expandFunsAll(json, editor.inputConnection) + val res = CoroutineScope(Dispatchers.IO).async { + try { + HttpUtils.fetchUrl(jsonExpanded) + } catch (e: IOException) { + "[ERROR: Unable to query " + jsonExpanded + ": " + e.localizedMessage + "]" + } catch (e: JSONException) { + "[ERROR: Unable to query " + jsonExpanded + ": " + e.localizedMessage + "]" + } + } + CoroutineScope(Dispatchers.IO).async { + editor.runOp(editor.replaceSel(res.await())) + } + return NO_OP + } + } +} + +fun main() = runBlocking { // this: CoroutineScope + launch { println(mathjs("1+2")) } + println("Hello") } \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditor.java b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditor.java index 4281ff7..319df34 100644 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditor.java +++ b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditor.java @@ -1,13 +1,11 @@ package ee.ioc.phon.android.speechutils.editor; -import static android.os.Build.VERSION_CODES; import static ee.ioc.phon.android.speechutils.editor.FunctionExpanderKt.expandFuns; import static ee.ioc.phon.android.speechutils.editor.FunctionExpanderKt.getSelectionAsRe; import android.content.ActivityNotFoundException; import android.content.Context; import android.os.AsyncTask; -import android.os.Build.VERSION; import android.text.TextUtils; import android.util.Pair; import android.view.KeyEvent; @@ -52,7 +50,7 @@ public class InputConnectionCommandEditor implements CommandEditor { private static final Pattern WHITESPACE_AND_TOKEN = Pattern.compile("\\s*\\w+"); private static final String F_SELECTION = "@sel()"; - private Context mContext; + private final Context mContext; private CharSequence mTextBeforeCursor; // TODO: Restrict the size of these stacks @@ -60,9 +58,9 @@ public class InputConnectionCommandEditor implements CommandEditor { // The command prefix is a list of consecutive final results whose concatenation can possibly // form a command. An item is added to the list for every final result that is not a command. // The list if cleared if a command is executed. - private List mCommandPrefix = new ArrayList<>(); - private Deque mOpStack = new ArrayDeque<>(); - private Deque mUndoStack = new ArrayDeque<>(); + private final List mCommandPrefix = new ArrayList<>(); + private final Deque mOpStack = new ArrayDeque<>(); + private final Deque mUndoStack = new ArrayDeque<>(); private InputConnection mInputConnection; @@ -678,10 +676,12 @@ public Op run() { CharSequence input = et.text.subSequence(0, et.selectionStart); // 0 == last match //String regexExpanded = expandFuns(regex, new SelFromExtractedText(et)); - //String regexExpanded = expandFuns(regex, new SelEvaluated(getSelectionAsRe(et).toString())); - //SelFromExtractedText sel = new SelFromExtractedText(et); + // String regexExpanded = expandFuns(regex, new SelEvaluated(getSelectionAsRe(et).toString())); + // SelFromExtractedText sel = new SelFromExtractedText(et); + String regexExpanded = regex.replace(F_SELECTION, getSelectionAsRe(et)); - //String regexExpanded = regex.replace(F_SELECTION, sel.apply(null)); + + // String regexExpanded = regex.replace(F_SELECTION, sel.apply(null)); Pair pos = matchNth(Pattern.compile(regexExpanded), input, 0); if (pos != null) { undo = getOpSetSelection(pos.first, pos.second, et.selectionStart, et.selectionEnd).run(); @@ -703,9 +703,11 @@ public Op run() { final ExtractedText et = getExtractedText(); if (et != null) { CharSequence input = et.text.subSequence(et.selectionEnd, et.text.length()); + String regexExpanded = regex.replace(F_SELECTION, getSelectionAsRe(et)); + //String regexExpanded2 = expandFuns(regex, new SelEvaluated(getSelectionAsRe(et).toString())); // TODO: sometimes crashes with: // StringIndexOutOfBoundsException: String index out of range: -4 - Pair pos = matchNth(Pattern.compile(regex.replace(F_SELECTION, getSelectionAsRe(et))), input, n); + Pair pos = matchNth(Pattern.compile(regexExpanded), input, n); if (pos != null) { undo = getOpSetSelection(et.selectionEnd + pos.first, et.selectionEnd + pos.second, et.selectionStart, et.selectionEnd).run(); } @@ -814,12 +816,21 @@ public Op run() { // Replace mentions of selection with a back-reference mInputConnection.beginBatchEdit(); // Change the current selection with the input argument, possibly embedding the selection. - String selectedText = getSelectedText(); + Sel sel = new Sel(mInputConnection); + //String selectedText = getSelectedText(); String newText; if (str == null || str.isEmpty()) { newText = ""; } else { - newText = FunctionExpanderKt.expandFuns2(str, selectedText, mInputConnection); + newText = expandFuns(str, + sel, + new Text(mInputConnection), + new Lower(), + new Upper(), + new UrlEncode(), + new GetUrl(), + new Expr(), + new Timestamp()); } Op op = null; if (regex != null) { @@ -830,14 +841,14 @@ public Op run() { int oldStart = et.startOffset + et.selectionStart; int oldEnd = et.startOffset + et.selectionEnd; Collection collection = new ArrayList<>(); - collection.add(getCommitTextOp(selectedText, newText)); + collection.add(getCommitTextOp(sel.getSelectedText(), newText)); collection.add(getOpSetSelection(oldStart + pair.first, oldStart + pair.second, oldStart, oldEnd)); op = combineOps(collection); } } // If no regex was provided or no match was found then just commit the replacement. if (op == null) { - op = getCommitTextOp(selectedText, newText); + op = getCommitTextOp(sel.getSelectedText(), newText); } Op undo = op.run(); mInputConnection.endBatchEdit(); @@ -1370,7 +1381,7 @@ public Op run() { private UtteranceRewriter.Rewrite applyCommand(String text) { int len = mCommandPrefix.size(); for (int i = Math.min(MAX_UTT_IN_COMMAND, len); i > 0; i--) { - List sublist = mCommandPrefix.subList(len - i, len); + List sublist = mCommandPrefix.subList(len - i, len); // TODO: sometimes sublist is empty? String possibleCommand = TextUtils.join(" ", sublist); if (possibleCommand.isEmpty()) { @@ -1411,10 +1422,7 @@ private UtteranceRewriter.Rewrite applyCommand(String text) { } private boolean deleteSurrounding(int beforeLength, int afterLength) { - if (VERSION.SDK_INT >= VERSION_CODES.N) { - return mInputConnection.deleteSurroundingTextInCodePoints(beforeLength, afterLength); - } - return mInputConnection.deleteSurroundingText(beforeLength, afterLength); + return mInputConnection.deleteSurroundingTextInCodePoints(beforeLength, afterLength); } /**