From 83f5f5942e660c4ced427256c286045d3677cb9c Mon Sep 17 00:00:00 2001
From: Li Guanglin <60415467+guang-lin@users.noreply.github.com>
Date: Tue, 12 Sep 2023 04:22:40 +0800
Subject: [PATCH] Line numbers improvements, by @harshad1 @guang-lin @gsantner
(PR #2090)
---
.../activity/DocumentEditAndViewFragment.java | 12 +-
.../markor/format/TextConverterBase.java | 31 +-
.../asciidoc/AsciidocTextConverter.java | 6 +-
.../binary/EmbedBinaryTextConverter.java | 4 +-
.../markor/format/csv/CsvTextConverter.java | 7 +-
.../markdown/MarkdownTextConverter.java | 24 +-
.../plaintext/PlaintextTextConverter.java | 8 +-
.../format/todotxt/TodoTxtTextConverter.java | 4 +-
.../wikitext/WikitextTextConverter.java | 15 +-
.../markor/frontend/NewFileDialog.java | 3 +
.../frontend/filesearch/FileSearchEngine.java | 22 +-
.../frontend/textview/HighlightingEditor.java | 341 +++++++++++-------
.../frontend/textview/TextViewUtils.java | 24 ++
.../gsantner/markor/model/AppSettings.java | 16 +
14 files changed, 316 insertions(+), 201 deletions(-)
diff --git a/app/src/main/java/net/gsantner/markor/activity/DocumentEditAndViewFragment.java b/app/src/main/java/net/gsantner/markor/activity/DocumentEditAndViewFragment.java
index 8da684a36..a9b8639da 100644
--- a/app/src/main/java/net/gsantner/markor/activity/DocumentEditAndViewFragment.java
+++ b/app/src/main/java/net/gsantner/markor/activity/DocumentEditAndViewFragment.java
@@ -200,7 +200,7 @@ public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
_hlEditor.setTextColor(_appSettings.getEditorForegroundColor());
_hlEditor.setGravity(_appSettings.isEditorStartEditingInCenter() ? Gravity.CENTER : Gravity.NO_GRAVITY);
_hlEditor.setHighlightingEnabled(_appSettings.getDocumentHighlightState(_document.getPath(), _hlEditor.getText()));
- _hlEditor.setLineNumbersEnabled(_appSettings.isLineNumbersEnabled());
+ _hlEditor.setLineNumbersEnabled(_appSettings.getDocumentLineNumbersEnabled(_document.getPath()));
_hlEditor.setAutoFormatEnabled(_appSettings.getDocumentAutoFormatEnabled(_document.getPath()));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Do not need to send contents to accessibility
@@ -459,7 +459,7 @@ public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
if (saveDocument(false)) {
TextConverterBase converter = FormatRegistry.getFormat(_document.getFormat(), activity, _document).getConverter();
_cu.shareText(getActivity(),
- converter.convertMarkup(getTextString(), getActivity(), false, _document.getFile()),
+ converter.convertMarkup(getTextString(), getActivity(), false, _hlEditor.getLineNumbersEnabled(), _document.getFile()),
"text/" + (item.getItemId() == R.id.action_share_html ? "html" : "plain")
);
}
@@ -563,9 +563,9 @@ public void onFsViewerConfig(GsFileBrowserOptions.Options dopt) {
return true;
}
case R.id.action_line_numbers: {
- _appSettings.setLineNumbersEnabled(!_appSettings.isLineNumbersEnabled());
- _hlEditor.setLineNumbersEnabled(_appSettings.isLineNumbersEnabled());
- _hlEditor.invalidate();
+ final boolean newState = !_hlEditor.getLineNumbersEnabled();
+ _appSettings.setDocumentLineNumbersEnabled(_document.getPath(), newState);
+ _hlEditor.setLineNumbersEnabled(newState);
updateMenuToggleStates(0);
return true;
}
@@ -758,7 +758,7 @@ private boolean isDisplayedAtMainActivity() {
public void updateViewModeText() {
final String text = getTextString();
- _format.getConverter().convertMarkupShowInWebView(_document, text, getActivity(), _webView, _nextConvertToPrintMode);
+ _format.getConverter().convertMarkupShowInWebView(_document, text, getActivity(), _webView, _nextConvertToPrintMode, _hlEditor.getLineNumbersEnabled());
}
public void setViewModeVisibility(boolean show) {
diff --git a/app/src/main/java/net/gsantner/markor/format/TextConverterBase.java b/app/src/main/java/net/gsantner/markor/format/TextConverterBase.java
index a41ff37ec..946ce4fa2 100644
--- a/app/src/main/java/net/gsantner/markor/format/TextConverterBase.java
+++ b/app/src/main/java/net/gsantner/markor/format/TextConverterBase.java
@@ -22,6 +22,7 @@
import net.gsantner.markor.model.Document;
import net.gsantner.opoc.format.GsTextUtils;
import net.gsantner.opoc.util.GsContextUtils;
+import net.gsantner.opoc.util.GsFileUtils;
import java.io.File;
import java.util.Date;
@@ -97,19 +98,18 @@ public TextConverterBase() {
* @param webView The WebView content to be shown in
* @return Copy of converted html
*/
- public String convertMarkupShowInWebView(Document document, String content, Activity context, WebView webView, boolean isExportInLightMode) {
+ public String convertMarkupShowInWebView(Document document, String content, Activity context, WebView webView, boolean lightMode, boolean lineNum) {
String html;
try {
- html = convertMarkup(content, context, isExportInLightMode, document.getFile());
+ html = convertMarkup(content, context, lightMode, lineNum, document.getFile());
} catch (Exception e) {
- html = "Please report at project issue tracker: " + e.toString();
+ html = "Please report at project issue tracker: " + e;
}
- String baseFolder = ApplicationObject.settings().getNotebookDirectory().getAbsolutePath();
- if (document.getFile().getParentFile() != null) {
- baseFolder = document.getFile().getParent();
+ String baseFolder = document.getFile().getParent();
+ if (baseFolder == null) {
+ baseFolder = "file://" + baseFolder + "/";
}
- baseFolder = "file://" + baseFolder + "/";
webView.loadDataWithBaseURL(baseFolder, html, getContentType(), UTF_CHARSET, null);
// When TOKEN_TEXT_CONVERTER_MAX_ZOOM_OUT_BY_DEFAULT is contained in text zoom out as far possible
@@ -121,21 +121,16 @@ public String convertMarkupShowInWebView(Document document, String content, Acti
return html;
}
- protected String getFileExtension(File file) {
- if (file == null) {
- return "";
- }
- return (file.getName().contains(".") ? file.getName().substring(file.getName().lastIndexOf(".")) : "").toLowerCase();
- }
-
/**
* Convert markup text to target format
*
- * @param markup Markup text
- * @param context Android Context
+ * @param markup Markup text
+ * @param context Android Context
+ * @param lightMode
+ * @param lineNum
* @return html as String
*/
- public abstract String convertMarkup(String markup, Context context, boolean isExportInLightMode, File file);
+ public abstract String convertMarkup(String markup, Context context, boolean lightMode, boolean lineNum, File file);
protected String putContentIntoTemplate(Context context, String content, boolean isExportInLightMode, File file, String onLoadJs, String head) {
final String contentLower = content.toLowerCase();
@@ -185,7 +180,7 @@ protected String putContentIntoTemplate(Context context, String content, boolean
.replace(TOKEN_ACCENT_COLOR, GsTextUtils.colorToHexString(ContextCompat.getColor(context, R.color.accent)))
.replace(TOKEN_TEXT_DIRECTION, _appSettings.isRenderRtl() ? "right" : "left")
.replace(TOKEN_FONT, font)
- .replace(TOKEN_TEXT_CONVERTER_CSS_CLASS, "format-" + getClass().getSimpleName().toLowerCase().replace("textconverter", "").replace("converter", "") + " fileext-" + getFileExtension(file).replace(".", ""))
+ .replace(TOKEN_TEXT_CONVERTER_CSS_CLASS, "format-" + getClass().getSimpleName().toLowerCase().replace("textconverter", "").replace("converter", "") + " fileext-" + GsFileUtils.getFilenameExtension(file).replace(".", ""))
.replace(TOKEN_POST_TODAY_DATE, DateFormat.getDateFormat(context).format(new Date()))
.replace(TOKEN_FILEURI_VIEWED_FILE, (file != null ? Uri.fromFile(file.getAbsoluteFile()).toString() : "file:///dummy").replace("'", "\\'").replace("\"", "\\\""));
diff --git a/app/src/main/java/net/gsantner/markor/format/asciidoc/AsciidocTextConverter.java b/app/src/main/java/net/gsantner/markor/format/asciidoc/AsciidocTextConverter.java
index 287ecc776..a2a8384e9 100644
--- a/app/src/main/java/net/gsantner/markor/format/asciidoc/AsciidocTextConverter.java
+++ b/app/src/main/java/net/gsantner/markor/format/asciidoc/AsciidocTextConverter.java
@@ -33,7 +33,7 @@ public class AsciidocTextConverter extends TextConverterBase {
public static final String HTML_ASCIIDOCJS_DARK_CSS_INCLUDE = "file:///android_asset/asciidoc/dark.css";
@Override
- public String convertMarkup(String markup, Context context, boolean isExportInLightMode, File file) {
+ public String convertMarkup(String markup, Context context, boolean lightMode, boolean lineNum, File file) {
String converted = "
\n";
String onLoadJs = "var textBase64 = `" +
//convert a text to base64 to simplify supporting special characters
@@ -52,10 +52,10 @@ public String convertMarkup(String markup, Context context, boolean isExportInLi
//standalone : true - to generate header 1 (= title) in the page. if don't do that - title will be absent.
//nofooter: true - to don't generate footer (Last updated ...). if don't do that and use standalone : true - the page will have that footer.
"var html = asciidoctor.convert(utf8PlainText, {standalone : true, attributes : {nofooter: true, stylesheet: \"" +
- (isExportInLightMode ? HTML_ASCIIDOCJS_DEFAULT_CSS_INCLUDE : HTML_ASCIIDOCJS_DARK_CSS_INCLUDE)
+ (lightMode ? HTML_ASCIIDOCJS_DEFAULT_CSS_INCLUDE : HTML_ASCIIDOCJS_DARK_CSS_INCLUDE)
+ "\"}});\n" +
"document.getElementById(\"asciidoc_content\").innerHTML = html;";
- return putContentIntoTemplate(context, converted, isExportInLightMode, file, onLoadJs, HTML_ASCIIDOCJS_JS_INCLUDE);
+ return putContentIntoTemplate(context, converted, lightMode, file, onLoadJs, HTML_ASCIIDOCJS_JS_INCLUDE);
}
@Override
diff --git a/app/src/main/java/net/gsantner/markor/format/binary/EmbedBinaryTextConverter.java b/app/src/main/java/net/gsantner/markor/format/binary/EmbedBinaryTextConverter.java
index 53b64c45a..56bf3e5a6 100644
--- a/app/src/main/java/net/gsantner/markor/format/binary/EmbedBinaryTextConverter.java
+++ b/app/src/main/java/net/gsantner/markor/format/binary/EmbedBinaryTextConverter.java
@@ -53,7 +53,7 @@ public class EmbedBinaryTextConverter extends TextConverterBase {
@SuppressWarnings({"ConstantConditions", "StringConcatenationInLoop"})
@Override
- public String convertMarkup(String markup, Context context, boolean isExportInLightMode, File file) {
+ public String convertMarkup(String markup, Context context, boolean lightMode, boolean lineNum, File file) {
String converted = "", onLoadJs = "", head = "";
if (file == null) {
return "";
@@ -152,7 +152,7 @@ public String convertMarkup(String markup, Context context, boolean isExportInLi
}
converted += HTML101_BODY_END;
- return putContentIntoTemplate(context, converted, isExportInLightMode, file, onLoadJs, head);
+ return putContentIntoTemplate(context, converted, lightMode, file, onLoadJs, head);
}
@Override
diff --git a/app/src/main/java/net/gsantner/markor/format/csv/CsvTextConverter.java b/app/src/main/java/net/gsantner/markor/format/csv/CsvTextConverter.java
index 1ae722618..7203bcc1d 100644
--- a/app/src/main/java/net/gsantner/markor/format/csv/CsvTextConverter.java
+++ b/app/src/main/java/net/gsantner/markor/format/csv/CsvTextConverter.java
@@ -18,6 +18,7 @@
import com.opencsv.CSVReaderBuilder;
import com.opencsv.ICSVParser;
+import net.gsantner.markor.format.TextConverterBase;
import net.gsantner.markor.format.markdown.MarkdownTextConverter;
import java.io.BufferedReader;
@@ -33,7 +34,7 @@
* Part of Markor-Architecture implementing Preview/Export for csv.
*
* Converts csv to md and let
- * {@link MarkdownTextConverter#convertMarkup(String, Context, boolean, File)}
+ * {@link TextConverterBase#convertMarkup(String, Context, boolean, boolean, File)}
* do the rest.
*
* This way csv columns may contain md expressions like bold text.
@@ -41,9 +42,9 @@
@SuppressWarnings("WeakerAccess")
public class CsvTextConverter extends MarkdownTextConverter {
@Override
- public String convertMarkup(String csvMarkup, Context context, boolean isExportInLightMode, File file) {
+ public String convertMarkup(String csvMarkup, Context context, boolean lightMode, boolean lineNum, File file) {
String mdMarkup = Csv2MdTable.toMdTable(csvMarkup);
- return super.convertMarkup(mdMarkup, context, isExportInLightMode, file);
+ return super.convertMarkup(mdMarkup, context, lightMode, lineNum, file);
}
@Override
diff --git a/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownTextConverter.java b/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownTextConverter.java
index 9371297ac..fe582dabe 100644
--- a/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownTextConverter.java
+++ b/app/src/main/java/net/gsantner/markor/format/markdown/MarkdownTextConverter.java
@@ -157,19 +157,20 @@ public class MarkdownTextConverter extends TextConverterBase {
//########################
@Override
- public String convertMarkup(String markup, Context context, boolean isExportInLightMode, File file) {
+ public String convertMarkup(String markup, Context context, boolean lightMode, boolean lineNum, File file) {
String converted = "", onLoadJs = "", head = "";
- MutableDataSet options = new MutableDataSet();
+ final MutableDataSet options = new MutableDataSet();
- if (_appSettings.isLineNumbersEnabled()) {
+ if (lineNum) {
// Add code blocks Line numbers extension
- ArrayList extensions = new ArrayList<>(flexmarkExtensions);
+ final ArrayList extensions = new ArrayList<>(flexmarkExtensions);
extensions.add(LineNumbersExtension.create());
options.set(Parser.EXTENSIONS, extensions);
} else {
options.set(Parser.EXTENSIONS, flexmarkExtensions);
}
+
options.set(Parser.SPACE_IN_LINK_URLS, true); // allow links like [this](some filename with spaces.md)
//options.set(HtmlRenderer.SOFT_BREAK, "
\n"); // Add linefeed to html break
options.set(EmojiExtension.USE_IMAGE_TYPE, EmojiImageType.UNICODE_ONLY); // Use unicode (OS/browser images)
@@ -273,8 +274,7 @@ public String convertMarkup(String markup, Context context, boolean isExportInLi
}
// Enable View (block) code syntax highlighting
- final String xt = getViewHlPrismIncludes((GsContextUtils.instance.isDarkModeEnabled(context) ? "-tomorrow" : ""));
- head += xt;
+ head += getViewHlPrismIncludes(GsContextUtils.instance.isDarkModeEnabled(context) ? "-tomorrow" : "", lineNum);
// Jekyll: Replace {{ site.baseurl }} with ..--> usually used in Jekyll blog _posts folder which is one folder below repository root, for reference to e.g. pictures in assets folder
markup = markup.replace("{{ site.baseurl }}", "..").replace(TOKEN_SITE_DATE_JEKYLL, TOKEN_POST_TODAY_DATE);
@@ -296,11 +296,9 @@ public String convertMarkup(String markup, Context context, boolean isExportInLi
fmaText = HTML_FRONTMATTER_CONTAINER_S + fmaText + HTML_FRONTMATTER_CONTAINER_E + "\n";
}
-
////////////
// Markup parsing - afterwards = HTML
- converted = flexmarkRenderer.withOptions(options).render(flexmarkParser.parse(markup));
- converted = fmaText + converted;
+ converted = fmaText + flexmarkRenderer.withOptions(options).render(flexmarkParser.parse(markup));
// After render changes: Fixes for Footnotes (converter creates footnote +
+ ref#(click) --> remove line break)
if (converted.contains("footnote-")) {
@@ -324,7 +322,7 @@ public String convertMarkup(String markup, Context context, boolean isExportInLi
}
// Deliver result
- return putContentIntoTemplate(context, converted, isExportInLightMode, file, onLoadJs, head);
+ return putContentIntoTemplate(context, converted, lightMode, file, onLoadJs, head);
}
private static final Pattern linkPattern = Pattern.compile("\\[(.*?)\\]\\((.*?)(\\s+\".*\")?\\)");
@@ -360,13 +358,13 @@ private String escapeSpacesInLink(final String markup) {
}
@SuppressWarnings({"StringConcatenationInsideStringBufferAppend"})
- private String getViewHlPrismIncludes(final String themeName) {
+ private String getViewHlPrismIncludes(final String themeName, final boolean lineNum) {
final StringBuilder sb = new StringBuilder(1000);
sb.append(CSS_PREFIX + "prism/themes/prism" + themeName + ".min.css" + CSS_POSTFIX);
sb.append(JS_PREFIX + "prism/prism.js" + JS_POSTFIX);
sb.append(JS_PREFIX + "prism/plugins/autoloader/prism-autoloader.min.js" + JS_POSTFIX);
- if (_appSettings.isLineNumbersEnabled()) {
+ if (lineNum) {
sb.append(CSS_PREFIX + "prism/plugins/line-numbers/prism-line-numbers.css" + CSS_POSTFIX);
sb.append(JS_PREFIX + "prism/plugins/line-numbers/prism-line-numbers.min.js" + JS_POSTFIX);
}
@@ -424,4 +422,4 @@ private String replaceTokens(final String markup, final Map
return markupReplaced;
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/net/gsantner/markor/format/plaintext/PlaintextTextConverter.java b/app/src/main/java/net/gsantner/markor/format/plaintext/PlaintextTextConverter.java
index 6eba2d0c7..bde06ccb2 100644
--- a/app/src/main/java/net/gsantner/markor/format/plaintext/PlaintextTextConverter.java
+++ b/app/src/main/java/net/gsantner/markor/format/plaintext/PlaintextTextConverter.java
@@ -44,7 +44,7 @@ public class PlaintextTextConverter extends TextConverterBase {
//########################
@Override
- public String convertMarkup(String markup, Context context, boolean isExportInLightMode, File file) {
+ public String convertMarkup(String markup, Context context, boolean lightMode, boolean lineNum, File file) {
String converted = "", onLoadJs = "", head = "";
final String extWithDot = GsFileUtils.getFilenameExtension(file);
String tmp;
@@ -65,12 +65,12 @@ public String convertMarkup(String markup, Context context, boolean isExportInLi
converted += markup;
} else if (extWithDot.matches(EmbedBinaryTextConverter.EXT_MATCHES_M3U_PLAYLIST)) {
// Playlist: Load in Embed-Binary view-mode
- return FormatRegistry.CONVERTER_EMBEDBINARY.convertMarkup(markup, context, isExportInLightMode, file);
+ return FormatRegistry.CONVERTER_EMBEDBINARY.convertMarkup(markup, context, lightMode, lineNum, file);
} else if (EXT_CODE_HL.contains(extWithDot) || (this instanceof KeyValueTextConverter)) {
// Source code: Load in Markdown view-mode & utilize code block highlighting
final String hlLang = extWithDot.replace(".sh", ".bash").replace(".", "");
markup = String.format(Locale.ROOT, "```%s\n%s\n```", hlLang, markup);
- return FormatRegistry.CONVERTER_MARKDOWN.convertMarkup(markup, context, isExportInLightMode, file);
+ return FormatRegistry.CONVERTER_MARKDOWN.convertMarkup(markup, context, lightMode, lineNum, file);
} else {
///////////////////////////////////////////
// Whatever else show in plaintext block
@@ -78,7 +78,7 @@ public String convertMarkup(String markup, Context context, boolean isExportInLi
+ TextUtilsCompat.htmlEncode(markup)
+ HTML101_BODY_PRE_END;
}
- return putContentIntoTemplate(context, converted, isExportInLightMode, file, onLoadJs, head);
+ return putContentIntoTemplate(context, converted, lightMode, file, onLoadJs, head);
}
@Override
diff --git a/app/src/main/java/net/gsantner/markor/format/todotxt/TodoTxtTextConverter.java b/app/src/main/java/net/gsantner/markor/format/todotxt/TodoTxtTextConverter.java
index 698515f6a..00c88216a 100644
--- a/app/src/main/java/net/gsantner/markor/format/todotxt/TodoTxtTextConverter.java
+++ b/app/src/main/java/net/gsantner/markor/format/todotxt/TodoTxtTextConverter.java
@@ -29,12 +29,12 @@ public class TodoTxtTextConverter extends TextConverterBase {
//########################
@Override
- public String convertMarkup(String markup, Context context, boolean isExportInLightMode, File file) {
+ public String convertMarkup(String markup, Context context, boolean lightMode, boolean lineNum, File file) {
String converted = "", onLoadJs = "", head = "";
converted = HTML100_BODY_PRE_BEGIN
+ parse(TextUtilsCompat.htmlEncode(markup))
+ HTML101_BODY_PRE_END;
- return putContentIntoTemplate(context, converted, isExportInLightMode, file, onLoadJs, head);
+ return putContentIntoTemplate(context, converted, lightMode, file, onLoadJs, head);
}
private String parse(String str) {
diff --git a/app/src/main/java/net/gsantner/markor/format/wikitext/WikitextTextConverter.java b/app/src/main/java/net/gsantner/markor/format/wikitext/WikitextTextConverter.java
index 033652c2d..c0598dec0 100644
--- a/app/src/main/java/net/gsantner/markor/format/wikitext/WikitextTextConverter.java
+++ b/app/src/main/java/net/gsantner/markor/format/wikitext/WikitextTextConverter.java
@@ -35,25 +35,26 @@ public class WikitextTextConverter extends TextConverterBase {
/**
* First, convert Wikitext to regular Markor markdown. Then, calls the regular converter.
*
- * @param markup Markup text
- * @param context Android Context
- * @param isExportInLightMode True if the light theme is to apply.
- * @param file The file to convert.
+ * @param markup Markup text
+ * @param context Android Context
+ * @param lightMode True if the light theme is to apply.
+ * @param lineNum
+ * @param file The file to convert.
* @return HTML text
*/
@Override
- public String convertMarkup(String markup, Context context, boolean isExportInLightMode, File file) {
+ public String convertMarkup(String markup, Context context, boolean lightMode, boolean lineNum, File file) {
String contentWithoutHeader = markup.replaceFirst(WikitextSyntaxHighlighter.ZIMHEADER.toString(), "");
StringBuilder markdownContent = new StringBuilder();
for (String line : contentWithoutHeader.split("\\r\\n|\\r|\\n")) {
- String markdownEquivalentLine = getMarkdownEquivalentLine(context, file, line, isExportInLightMode);
+ String markdownEquivalentLine = getMarkdownEquivalentLine(context, file, line, lightMode);
markdownContent.append(markdownEquivalentLine);
markdownContent.append(" "); // line breaks must be made explicit in markdown by two spaces
markdownContent.append(String.format("%n"));
}
- return FormatRegistry.CONVERTER_MARKDOWN.convertMarkup(markdownContent.toString(), context, isExportInLightMode, file);
+ return FormatRegistry.CONVERTER_MARKDOWN.convertMarkup(markdownContent.toString(), context, lightMode, lineNum, file);
}
private String getMarkdownEquivalentLine(final Context context, final File file, String wikitextLine, final boolean isExportInLightMode) {
diff --git a/app/src/main/java/net/gsantner/markor/frontend/NewFileDialog.java b/app/src/main/java/net/gsantner/markor/frontend/NewFileDialog.java
index 4e31122e8..5b4d8433e 100644
--- a/app/src/main/java/net/gsantner/markor/frontend/NewFileDialog.java
+++ b/app/src/main/java/net/gsantner/markor/frontend/NewFileDialog.java
@@ -348,6 +348,9 @@ private Pair getTemplateContent(final Spinner templateSpinner,
final int startingIndex = t.indexOf(HighlightingEditor.PLACE_CURSOR_HERE_TOKEN);
t = t.replace(HighlightingEditor.PLACE_CURSOR_HERE_TOKEN, "");
+ // Has no utility in a new file
+ t = t.replace(HighlightingEditor.INSERT_SELECTION_HERE_TOKEN, "");
+
final byte[] bytes;
if (encrypt && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
final char[] pass = ApplicationObject.settings().getDefaultPassword();
diff --git a/app/src/main/java/net/gsantner/markor/frontend/filesearch/FileSearchEngine.java b/app/src/main/java/net/gsantner/markor/frontend/filesearch/FileSearchEngine.java
index dec1a0f2d..14575b017 100644
--- a/app/src/main/java/net/gsantner/markor/frontend/filesearch/FileSearchEngine.java
+++ b/app/src/main/java/net/gsantner/markor/frontend/filesearch/FileSearchEngine.java
@@ -233,17 +233,14 @@ private Queue currentDirectoryHandler(final File currentDir) {
final boolean isDir = f.isDirectory();
- if (!isDir && f.canRead()) {
-
- final int beforeContentCount = _result.size();
- if (_config.isSearchInContent && GsFileUtils.isTextFile(f)) {
- getContentMatches(f, _config.isOnlyFirstContentMatch, trimSize);
- }
+ final int beforeContentCount = _result.size();
+ if (_config.isSearchInContent && !isDir && f.canRead() && GsFileUtils.isTextFile(f)) {
+ getContentMatches(f, _config.isOnlyFirstContentMatch, trimSize);
+ }
- // Search name if not already included due to content
- if (_result.size() == beforeContentCount) {
- getFileIfNameMatches(f, trimSize);
- }
+ // Search name if director or not already included due to content
+ if (isDir || _result.size() == beforeContentCount) {
+ getFileIfNameMatches(f, trimSize);
}
if (isDir && !isFileContainSymbolicLinks(f, currentDir)) {
@@ -251,7 +248,6 @@ private Queue currentDirectoryHandler(final File currentDir) {
}
}
-
publishProgress(_currentQueueLength + subQueue.size(), _currentSearchDepth, _result.size(), _countCheckedFiles);
}
} catch (Exception ignored) {
@@ -332,11 +328,11 @@ private boolean isFileContainSymbolicLinks(File file, File expectedParentDir) {
return true;
}
- private void getFileIfNameMatches(final File file, final int trim) {
+ private void getFileIfNameMatches(final File file, final int baseLength) {
try {
final String fileName = _config.isCaseSensitiveQuery ? file.getName() : file.getName().toLowerCase();
if (_config.isRegexQuery ? _matcher.reset(fileName).matches() : fileName.contains(_config.query)) {
- _result.add(new FitFile(file.getCanonicalPath().substring(trim), file.isDirectory(), null));
+ _result.add(new FitFile(file.getCanonicalPath().substring(baseLength), file.isDirectory(), null));
}
} catch (Exception ignored) {
}
diff --git a/app/src/main/java/net/gsantner/markor/frontend/textview/HighlightingEditor.java b/app/src/main/java/net/gsantner/markor/frontend/textview/HighlightingEditor.java
index b01497717..90f9db31e 100644
--- a/app/src/main/java/net/gsantner/markor/frontend/textview/HighlightingEditor.java
+++ b/app/src/main/java/net/gsantner/markor/frontend/textview/HighlightingEditor.java
@@ -9,7 +9,6 @@
import android.content.Context;
import android.graphics.Canvas;
-import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Build;
@@ -23,7 +22,6 @@
import android.view.View;
import android.view.ViewTreeObserver;
import android.view.accessibility.AccessibilityEvent;
-import android.widget.ScrollView;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
@@ -35,9 +33,6 @@
import net.gsantner.opoc.wrapper.GsCallback;
import net.gsantner.opoc.wrapper.GsTextWatcherAdapter;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
@SuppressWarnings("UnusedReturnValue")
public class HighlightingEditor extends AppCompatEditText {
@@ -45,6 +40,7 @@ public class HighlightingEditor extends AppCompatEditText {
final static float HIGHLIGHT_REGION_SIZE = 0.75f; // Minimum extra screens to highlight (should be > 0.5 to cover screen)
public final static String PLACE_CURSOR_HERE_TOKEN = "%%PLACE_CURSOR_HERE%%";
+ public final static String INSERT_SELECTION_HERE_TOKEN = "%%INSERT_SELECTION_HERE%%";
private boolean _accessibilityEnabled = true;
private final boolean _isSpellingRedUnderline;
@@ -52,7 +48,7 @@ public class HighlightingEditor extends AppCompatEditText {
private boolean _isDynamicHighlightingEnabled = true;
private Runnable _hlDebounced; // Debounced runnable which recomputes highlighting
private boolean _hlEnabled; // Whether highlighting is enabled
- private boolean _nuEnabled; // Whether show line numbers is enabled
+ private boolean _numEnabled; // Whether show line numbers is enabled
private final Rect _oldHlRect; // Rect highlighting was previously applied to
private final Rect _hlRect; // Current rect
private int _hlShiftThreshold = -1; // How much to scroll before re-apply highlight
@@ -60,17 +56,7 @@ public class HighlightingEditor extends AppCompatEditText {
private TextWatcher _autoFormatModifier;
private boolean _autoFormatEnabled;
private boolean _saveInstanceState = true;
-
- // For drawing line numbers
- private final Paint _paint = new Paint();
- private ScrollView _scrollView;
- private int _x;
- private int _maxLineNumber = 1;
- private int _maxLineNumberWidth;
- private int _defaultPaddingLeft;
- private static final int LINE_NUMBERS_PADDING_LEFT = 14;
- private static final int LINE_NUMBERS_PADDING_RIGHT = 10;
- private final int[] _firstVisibleLine = {-1, 0}; // {line index, actual line number}
+ private final LineNumbersDrawer _lineNumbersDrawer = new LineNumbersDrawer(this);
public HighlightingEditor(Context context, AttributeSet attrs) {
@@ -86,38 +72,17 @@ public HighlightingEditor(Context context, AttributeSet attrs) {
}
_hlEnabled = false;
- _nuEnabled = false;
+ _numEnabled = false;
_oldHlRect = new Rect();
_hlRect = new Rect();
addTextChangedListener(new GsTextWatcherAdapter() {
- private final Pattern pattern = Pattern.compile("\n");
- private Matcher matcher;
-
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- if (after == 0 && count > 0) {
- CharSequence deleted = s.subSequence(start, start + count);
- matcher = pattern.matcher(deleted);
- while (matcher.find()) {
- _maxLineNumber--;
- }
- }
- }
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (_hlEnabled && _hl != null) {
_hl.fixup(start, before, count);
}
-
- if (before == 0 && count > 0) {
- CharSequence added = s.subSequence(start, start + count);
- matcher = pattern.matcher(added);
- while (matcher.find()) {
- _maxLineNumber++;
- }
- }
}
@Override
@@ -133,100 +98,22 @@ public void afterTextChanged(final Editable s) {
observer.addOnScrollChangedListener(() -> updateHighlighting(false));
observer.addOnGlobalLayoutListener(() -> updateHighlighting(false));
- // Fix for android 12 perf issues - https://github.com/gsantner/markor/discussions/1794
+ // Fix for Android 12 perf issues - https://github.com/gsantner/markor/discussions/1794
setEmojiCompatEnabled(false);
}
- @Override
- protected void onFinishInflate() {
- super.onFinishInflate();
- _defaultPaddingLeft = getPaddingLeft();
- _paint.setTextAlign(Paint.Align.RIGHT);
- }
-
- @Override
- protected void onAttachedToWindow() {
- super.onAttachedToWindow();
- _scrollView = getParent() instanceof ScrollView ? (ScrollView) getParent() : (ScrollView) getParent().getParent();
- _scrollView.setOnScrollChangeListener((v, scrollX, scrollY, oldScrollX, oldScrollY) -> {
- if (_firstVisibleLine[0] > -1) {
- _firstVisibleLine[0] = -1;
- }
- });
- }
-
@Override
public boolean onPreDraw() {
- _paint.setTextSize(getTextSize());
+ _lineNumbersDrawer.setTextSize(getTextSize());
return super.onPreDraw();
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
- // If line numbers can be drawn
- if (_nuEnabled && _maxLineNumber < (AppSettings._isDeviceGoodHardware ? 5000 : 3000)) {
- drawLineNumbers(canvas);
- } else if (getPaddingLeft() != _defaultPaddingLeft) {
- _maxLineNumberWidth = 0;
- // Reset padding without line numbers fence
- setPadding(_defaultPaddingLeft, getPaddingTop(), getPaddingRight(), getPaddingBottom());
- }
- }
-
- private void drawLineNumbers(Canvas canvas) {
- final Editable text = getText();
- final Layout layout = getLayout();
- final float offsetY = getPaddingTop();
- final int top = _scrollView.getScrollY() - 100; // Top of current visible area
- final int bottom = _scrollView.getScrollY() + _scrollView.getHeight(); // Bottom of current visible area
- final int width = (int) _paint.measureText(String.valueOf(_maxLineNumber));
- if (_maxLineNumberWidth != width) {
- _maxLineNumberWidth = width;
- _x = LINE_NUMBERS_PADDING_LEFT + width;
- setPadding(_x + LINE_NUMBERS_PADDING_RIGHT + 10, getPaddingTop(), getPaddingRight(), getPaddingBottom());
- }
-
- // Draw the vertical line
- _paint.setColor(Color.LTGRAY);
- canvas.drawLine(_x + LINE_NUMBERS_PADDING_RIGHT, top, _x + LINE_NUMBERS_PADDING_RIGHT, bottom, _paint);
- // Draw line numbers
- _paint.setColor(Color.GRAY);
- canvas.drawText("1", _x, layout.getLineBounds(0, null) + offsetY, _paint);
-
- if (text == null || text.length() == 0) {
- return;
- }
-
- final int count = getLineCount();
- int i = 1, number = 1;
-
- if (_firstVisibleLine[0] > -1) {
- // Set and then iterate from the first visible line
- i = _firstVisibleLine[0];
- number = _firstVisibleLine[1];
- } else {
- // Set the first visible line invalid, it needs to be updated
- _firstVisibleLine[0] = -1;
- }
-
- for (int y; i < count; i++) {
- if (text.charAt(layout.getLineStart(i) - 1) == '\n') {
- number++;
- y = layout.getLineBounds(i, null);
- if (y > bottom) {
- break;
- }
- if (y > top) {
- if (_firstVisibleLine[0] < 0) {
- // Update the first visible line
- _firstVisibleLine[0] = i;
- _firstVisibleLine[1] = number - 1;
- }
- canvas.drawText(String.valueOf(number), _x, y + offsetY, _paint);
- }
- }
+ if (_numEnabled) {
+ _lineNumbersDrawer.draw(canvas);
}
}
@@ -245,6 +132,7 @@ private void updateHighlighting(final boolean recompute) {
// Don't highlight unless shifted sufficiently or a recompute is required
if (recompute || (visible && _hl.hasSpans() && isScrollSignificant())) {
+ _oldHlRect.set(_hlRect);
final int[] newHlRegion = hlRegion(_hlRect); // Compute this _before_ clear
_hl.clearDynamic();
@@ -314,16 +202,20 @@ public boolean setHighlightingEnabled(final boolean enable) {
}
public boolean getLineNumbersEnabled() {
- return _nuEnabled;
+ return _numEnabled;
}
- public boolean setLineNumbersEnabled(final boolean enable) {
- final boolean prev = _nuEnabled;
-
- if (enable != _nuEnabled) {
- _nuEnabled = enable;
+ public void setLineNumbersEnabled(final boolean enable) {
+ if (enable ^ _numEnabled) {
+ post(this::invalidate);
+ }
+ _numEnabled = enable;
+ if (_numEnabled) {
+ _lineNumbersDrawer.startLineTracking();
+ } else {
+ _lineNumbersDrawer.reset();
+ _lineNumbersDrawer.stopLineTracking();
}
- return prev;
}
// Region to highlight
@@ -338,6 +230,11 @@ private int[] hlRegion(final Rect rect) {
}
}
+ @Override
+ public boolean bringPointIntoView(int i) {
+ return super.bringPointIntoView(i);
+ }
+
private int rowStart(final int y) {
final Layout layout = getLayout();
final int line = layout.getLineForVertical(y);
@@ -508,13 +405,30 @@ public void simulateKeyPress(int keyEvent_KEYCODE_SOMETHING) {
public void insertOrReplaceTextOnCursor(final String newText) {
final Editable edit = getText();
if (edit != null && newText != null) {
- final int newCursorPos = newText.indexOf(PLACE_CURSOR_HERE_TOKEN);
- final String finalText = newText.replace(PLACE_CURSOR_HERE_TOKEN, "");
+
+ // TODO - should consider moving any snippet specific logic out of here
+ // Fill in any instances of selection
final int[] sel = TextViewUtils.getSelection(this);
+ final CharSequence selected = TextViewUtils.toString(edit, sel[0], sel[1]);
+ String expanded = newText.replace(INSERT_SELECTION_HERE_TOKEN, selected);
+
+ // Determine where to place the cursor
+ final int newCursorPos = expanded.indexOf(PLACE_CURSOR_HERE_TOKEN);
+ final String finalText = expanded.replace(PLACE_CURSOR_HERE_TOKEN, "");
+
sel[0] = Math.max(sel[0], 0);
+
+ // Needed to prevent selection of whole of inserted text after replace
+ // if we want a cursor position instead
+ if (newCursorPos >= 0) {
+ setSelection(sel[0]);
+ }
+
withAutoFormatDisabled(() -> edit.replace(sel[0], sel[1], finalText));
+
if (newCursorPos >= 0) {
setSelection(sel[0] + newCursorPos);
+ TextViewUtils.showSelection(this);
}
}
}
@@ -546,4 +460,171 @@ public int setSelectionExpandWholeLines() {
public boolean indexesValid(int... indexes) {
return TextViewUtils.inRange(0, length(), indexes);
}
+
+ static class LineNumbersDrawer {
+
+ private final AppCompatEditText _editor;
+ private final Paint _paint = new Paint();
+
+ private final int _defaultPaddingLeft;
+ private static final int LINE_NUMBER_PADDING_LEFT = 14;
+ private static final int LINE_NUMBER_PADDING_RIGHT = 10;
+
+ private final Rect _visibleArea = new Rect();
+ private final Rect _lineNumbersArea = new Rect();
+
+ private int _numberX;
+ private int _gutterX;
+ private int _maxNumber = 1; // to gauge gutter width
+ private int _maxNumberDigits;
+ private float _oldTextSize;
+ private final int[] _startLine = {0, 1}; // {line index, actual line number}
+
+ private final GsTextWatcherAdapter _lineTrackingWatcher = new GsTextWatcherAdapter() {
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ _maxNumber -= TextViewUtils.countChar(s, start, start + count, '\n');
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ _maxNumber += TextViewUtils.countChar(s, start, start + count, '\n');
+ }
+ };
+
+ public LineNumbersDrawer(final AppCompatEditText editor) {
+ _editor = editor;
+ _paint.setColor(0xFF999999);
+ _paint.setTextAlign(Paint.Align.RIGHT);
+ _defaultPaddingLeft = editor.getPaddingLeft();
+ }
+
+ public void setTextSize(final float textSize) {
+ _paint.setTextSize(textSize);
+ }
+
+ public boolean isTextSizeChanged() {
+ if (_paint.getTextSize() == _oldTextSize) {
+ return false;
+ } else {
+ _oldTextSize = _paint.getTextSize();
+ return true;
+ }
+ }
+
+ public boolean isMaxNumberDigitsChanged() {
+ final int oldDigits = _maxNumberDigits;
+
+ if (_maxNumber < 10) {
+ _maxNumberDigits = 1;
+ } else if (_maxNumber < 100) {
+ _maxNumberDigits = 2;
+ } else if (_maxNumber < 1000) {
+ _maxNumberDigits = 3;
+ } else if (_maxNumber < 10000) {
+ _maxNumberDigits = 4;
+ } else {
+ _maxNumberDigits = 5;
+ }
+ return _maxNumberDigits != oldDigits;
+ }
+
+ public boolean isOutOfLineNumbersArea() {
+ final int margin = (int) (_visibleArea.height() * 0.5f);
+ final int top = _visibleArea.top - margin;
+ final int bottom = _visibleArea.bottom + margin;
+
+ if (top < _lineNumbersArea.top || bottom > _lineNumbersArea.bottom) {
+ // Reset line numbers area
+ // height of line numbers area = (1.5 + 1 + 1.5) * height of visible area
+ _lineNumbersArea.top = top - _visibleArea.height();
+ _lineNumbersArea.bottom = bottom + _visibleArea.height();
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public void startLineTracking() {
+ _editor.removeTextChangedListener(_lineTrackingWatcher);
+ _maxNumber = 1;
+ final CharSequence text = _editor.getText();
+ if (text != null) {
+ _maxNumber += TextViewUtils.countChar(text, 0, text.length(), '\n');
+ }
+ _editor.addTextChangedListener(_lineTrackingWatcher);
+ }
+
+ public void stopLineTracking() {
+ _editor.removeTextChangedListener(_lineTrackingWatcher);
+ }
+
+ /**
+ * Draw line numbers.
+ *
+ * @param canvas The canvas on which the line numbers will be drawn.
+ */
+ public void draw(final Canvas canvas) {
+ if (!_editor.getLocalVisibleRect(_visibleArea)) {
+ return;
+ }
+
+ final CharSequence text = _editor.getText();
+ final Layout layout = _editor.getLayout();
+ if (text == null || layout == null) {
+ return;
+ }
+
+ // If text size or the max line number of digits changed,
+ // update the variables and reset padding
+ if (isTextSizeChanged() || isMaxNumberDigitsChanged()) {
+ _numberX = LINE_NUMBER_PADDING_LEFT + (int) _paint.measureText(String.valueOf(_maxNumber));
+ _gutterX = _numberX + LINE_NUMBER_PADDING_RIGHT;
+ _editor.setPadding(_gutterX + 10, _editor.getPaddingTop(), _editor.getPaddingRight(), _editor.getPaddingBottom());
+ }
+
+ int i = _startLine[0], number = _startLine[1];
+ // If current visible area is out of current line numbers area,
+ // iterate from the first line to recalculate the start line
+ if (isOutOfLineNumbersArea()) {
+ i = 0;
+ number = 1;
+ _startLine[0] = -1;
+ }
+
+ // Draw border of the gutter
+ canvas.drawLine(_gutterX, _lineNumbersArea.top, _gutterX, _lineNumbersArea.bottom, _paint);
+
+ // Draw line numbers
+ final int count = layout.getLineCount();
+ final int offsetY = _editor.getPaddingTop();
+ for (; i < count; i++) {
+ final int start = layout.getLineStart(i);
+ if (start == 0 || text.charAt(start - 1) == '\n') {
+ final int y = layout.getLineBaseline(i);
+ if (y > _lineNumbersArea.bottom) {
+ break;
+ }
+ if (y > _lineNumbersArea.top) {
+ if (_startLine[0] < 0) {
+ _startLine[0] = i;
+ _startLine[1] = number;
+ }
+ canvas.drawText(String.valueOf(number), _numberX, y + offsetY, _paint);
+ }
+ number++;
+ }
+ }
+ }
+
+ /**
+ * Reset to the state without line numbers.
+ */
+ public void reset() {
+ if (_editor.getPaddingLeft() != _defaultPaddingLeft) {
+ _editor.setPadding(_defaultPaddingLeft, _editor.getPaddingTop(), _editor.getPaddingRight(), _editor.getPaddingBottom());
+ _maxNumberDigits = 0;
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/net/gsantner/markor/frontend/textview/TextViewUtils.java b/app/src/main/java/net/gsantner/markor/frontend/textview/TextViewUtils.java
index 81313b926..f9c2227ae 100644
--- a/app/src/main/java/net/gsantner/markor/frontend/textview/TextViewUtils.java
+++ b/app/src/main/java/net/gsantner/markor/frontend/textview/TextViewUtils.java
@@ -214,6 +214,11 @@ public static int[] countChars(final CharSequence s, final char... chars) {
* @return number of instances of each char in [start, end)
*/
public static int[] countChars(final CharSequence s, int start, int end, final char... chars) {
+ // Faster specialization for the common single case
+ if (chars.length == 1) {
+ return new int[] {countChar(s, start, end, chars[0])};
+ }
+
final int[] counts = new int[chars.length];
start = Math.max(0, start);
end = Math.min(end, s.length());
@@ -228,6 +233,25 @@ public static int[] countChars(final CharSequence s, int start, int end, final c
return counts;
}
+ public static int countChar(final CharSequence s, final char c) {
+ return countChar(s, 0, s.length(), c);
+ }
+
+ /**
+ * Count instances of a single char in a charsequence
+ */
+ public static int countChar(final CharSequence s, int start, int end, final char c) {
+ start = Math.max(0, start);
+ end = Math.min(end, s.length());
+ int count = 0;
+ for (int i = start; i < end; i++) {
+ if (s.charAt(i) == c) {
+ count++;
+ }
+ }
+ return count;
+ }
+
public static boolean isNewLine(CharSequence source, int start, int end) {
return isValidIndex(source, start, end - 1) && (source.charAt(start) == '\n' || source.charAt(end - 1) == '\n');
}
diff --git a/app/src/main/java/net/gsantner/markor/model/AppSettings.java b/app/src/main/java/net/gsantner/markor/model/AppSettings.java
index 484e155c8..baec60b15 100644
--- a/app/src/main/java/net/gsantner/markor/model/AppSettings.java
+++ b/app/src/main/java/net/gsantner/markor/model/AppSettings.java
@@ -357,6 +357,7 @@ public void toggleFavouriteFile(File file) {
private static final String PREF_PREFIX_VIEW_SCROLL_X = "PREF_PREFIX_VIEW_SCROLL_X";
private static final String PREF_PREFIX_VIEW_SCROLL_Y = "PREF_PREFIX_VIEW_SCROLL_Y";
private static final String PREF_PREFIX_TODO_DONE_NAME = "PREF_PREFIX_TODO_DONE_NAME";
+ private static final String PREF_PREFIX_LINE_NUM_STATE = "PREF_PREFIX_LINE_NUM_STATE";
public void setLastTodoDoneName(final String path, final String name) {
if (fexists(path)) {
@@ -412,6 +413,21 @@ public boolean getDocumentWrapState(final String path) {
}
}
+ public void setDocumentLineNumbersEnabled(final String path, final boolean enabled) {
+ if (fexists(path)) {
+ setBool(PREF_PREFIX_LINE_NUM_STATE + path, enabled);
+ }
+ }
+
+ public boolean getDocumentLineNumbersEnabled(final String path) {
+ final boolean _default = false;
+ if (!fexists(path)) {
+ return _default;
+ } else {
+ return getBool(PREF_PREFIX_LINE_NUM_STATE + path, _default);
+ }
+ }
+
public void setDocumentFormat(final String path, @StringRes final int format) {
if (fexists(path) && format != FormatRegistry.FORMAT_UNKNOWN) {
setString(PREF_PREFIX_FILE_FORMAT + path, _context.getString(format));