├── .gitignore ├── .idea ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── libs │ └── JavaPrettify-1.2.1.jar ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── livejournal │ │ └── karino2 │ │ └── zipsourcecodereading │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── com │ │ │ ├── google │ │ │ └── re2j │ │ │ │ ├── CharClass.java │ │ │ │ ├── CharGroup.java │ │ │ │ ├── Compiler.java │ │ │ │ ├── DFA.java │ │ │ │ ├── DFAMachine.java │ │ │ │ ├── DFAState.java │ │ │ │ ├── DFAStateKey.java │ │ │ │ ├── Inst.java │ │ │ │ ├── Machine.java │ │ │ │ ├── MachineInput.java │ │ │ │ ├── Matcher.java │ │ │ │ ├── NFAMachine.java │ │ │ │ ├── Options.java │ │ │ │ ├── Parser.java │ │ │ │ ├── Pattern.java │ │ │ │ ├── PatternSyntaxException.java │ │ │ │ ├── Prog.java │ │ │ │ ├── RE2.java │ │ │ │ ├── Regexp.java │ │ │ │ ├── Simplify.java │ │ │ │ ├── SliceUtils.java │ │ │ │ ├── SparseSet.java │ │ │ │ ├── Unicode.java │ │ │ │ ├── UnicodeTables.java │ │ │ │ ├── Utils.java │ │ │ │ ├── make_perl_groups.pl │ │ │ │ ├── make_unicode_tables.awk │ │ │ │ └── package.html │ │ │ └── livejournal │ │ │ └── karino2 │ │ │ └── zipsourcecodereading │ │ │ ├── IndexingService.kt │ │ │ ├── MainActivity.kt │ │ │ ├── ObserverAdapter.kt │ │ │ ├── Query.kt │ │ │ ├── QueryOp.kt │ │ │ ├── RegexpInfo.kt │ │ │ ├── RegexpReader.kt │ │ │ ├── RxPrettifyParser.kt │ │ │ ├── SearchActivity.kt │ │ │ ├── Slice.java │ │ │ ├── SourceArchive.kt │ │ │ ├── SourceViewActivity.kt │ │ │ ├── ZipChooseActivity.kt │ │ │ ├── ZipEntryAux.kt │ │ │ ├── ZipFilerActivity.kt │ │ │ ├── index │ │ │ ├── Main.kt │ │ │ ├── Read.kt │ │ │ └── Write.kt │ │ │ └── text │ │ │ ├── ArrayUtils.kt │ │ │ ├── FastScroller.kt │ │ │ ├── HandleView.kt │ │ │ ├── Layout.kt │ │ │ ├── LongTextView.kt │ │ │ ├── MovementMethod.kt │ │ │ ├── NOSpannableString.kt │ │ │ ├── README.md │ │ │ ├── SelectionController.kt │ │ │ ├── Styled.kt │ │ │ └── TextUtils.kt │ └── res │ │ ├── drawable │ │ ├── cancel.png │ │ ├── choose.png │ │ ├── filer.png │ │ ├── gsearch.png │ │ ├── next.png │ │ ├── prev.png │ │ ├── scrollbar_handle.png │ │ ├── text_select_handle_left.png │ │ └── text_select_handle_right.png │ │ ├── layout │ │ ├── activity_search.xml │ │ ├── activity_source_view.xml │ │ ├── activity_zip_choose.xml │ │ ├── activity_zip_filer.xml │ │ ├── list_item.xml │ │ ├── search_bar.xml │ │ └── search_result_item.xml │ │ ├── menu │ │ ├── context_longtextview.xml │ │ ├── search.xml │ │ └── source_view.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ └── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ ├── java │ └── com │ │ └── livejournal │ │ └── karino2 │ │ └── zipsourcecodereading │ │ ├── ExampleUnitTest.java │ │ ├── IndexText.kt │ │ ├── QueryTest.kt │ │ ├── RegexpReaderTest.kt │ │ └── index │ │ └── WriteTest.kt │ └── resources │ └── MeatPieDay.idx ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── zscr.png /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .externalNativeBuild 10 | app/release/ 11 | .idea/caches/ 12 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 37 | 38 | 39 | 40 | 41 | 42 | 44 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZSCReading 2 | 3 | Source code reading app for Android. This app assumes target source codes are archived as one zip file. 4 | 5 | This app first indexing whole zip contents, then perform regular expression query. 6 | This app is Android port of [Code Search](https://github.com/google/codesearch) + GUI client. 7 | 8 | ## ZSCReading is built using open source software: 9 | 10 | - io.reactivex.rxjava2:rxjava 11 | - io.reactivex.rxjava2:rxandroid 12 | - [re2j-td](https://github.com/sopel39/re2j-td) we forked to use byte array and run on Android 13 | - JavaPrettify 14 | 15 | Also, we use codeserach logic. 16 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | 4 | android { 5 | compileSdkVersion 27 6 | buildToolsVersion '28.0.3' 7 | defaultConfig { 8 | applicationId "com.livejournal.karino2.zipsourcecodereading" 9 | minSdkVersion 19 10 | targetSdkVersion 27 11 | versionCode 3 12 | versionName "0.3" 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | } 15 | buildTypes { 16 | release { 17 | minifyEnabled false 18 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 19 | } 20 | debug { 21 | applicationIdSuffix ".debug" 22 | } 23 | 24 | debugRelease { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 27 | signingConfig signingConfigs.debug 28 | } 29 | 30 | } 31 | } 32 | 33 | dependencies { 34 | implementation fileTree(include: ['*.jar'], dir: 'libs') 35 | androidTestImplementation('com.android.support.test.espresso:espresso-core:2.2.2', { 36 | exclude group: 'com.android.support', module: 'support-annotations' 37 | }) 38 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 39 | implementation 'com.android.support:appcompat-v7:27.1.1' 40 | implementation 'com.android.support:recyclerview-v7:27.1.1' 41 | implementation 'com.android.support:support-compat:27.1.1' 42 | implementation 'com.android.support.constraint:constraint-layout:1.0.2' 43 | implementation 'io.reactivex.rxjava2:rxjava:2.1.3' 44 | implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' 45 | testImplementation 'junit:junit:4.12' 46 | } 47 | repositories { 48 | mavenCentral() 49 | } 50 | -------------------------------------------------------------------------------- /app/libs/JavaPrettify-1.2.1.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karino2/ZipSourceCodeReading/fee6b82247e233b0f816e30a4e68e6c9ed0b76d9/app/libs/JavaPrettify-1.2.1.jar -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:\Users\_\AppData\Local\Android\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/livejournal/karino2/zipsourcecodereading/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading; 2 | 3 | import android.content.Context; 4 | import android.support.test.InstrumentationRegistry; 5 | import android.support.test.runner.AndroidJUnit4; 6 | 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | 10 | import static org.junit.Assert.*; 11 | 12 | /** 13 | * Instrumentation test, which will execute on an Android device. 14 | * 15 | * @see Testing documentation 16 | */ 17 | @RunWith(AndroidJUnit4.class) 18 | public class ExampleInstrumentedTest { 19 | @Test 20 | public void useAppContext() throws Exception { 21 | // Context of the app under test. 22 | Context appContext = InstrumentationRegistry.getTargetContext(); 23 | 24 | assertEquals("karino2.livejournal.com.zipsourcecodereading", appContext.getPackageName()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 32 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/re2j/CharGroup.java: -------------------------------------------------------------------------------- 1 | // GENERATED BY make_perl_groups.pl; DO NOT EDIT. 2 | // make_perl_groups.pl >perl_groups.go 3 | 4 | package com.google.re2j; 5 | 6 | import java.util.HashMap; 7 | 8 | class CharGroup { 9 | 10 | final int sign; 11 | final int[] cls; 12 | 13 | private CharGroup(int sign, int[] cls) { 14 | this.sign = sign; 15 | this.cls = cls; 16 | } 17 | 18 | private static final int[] code1 = { /* \d */ 19 | 0x30, 0x39, 20 | }; 21 | 22 | private static final int[] code2 = { /* \s */ 23 | 0x9, 0xa, 24 | 0xc, 0xd, 25 | 0x20, 0x20, 26 | }; 27 | 28 | private static final int[] code3 = { /* \w */ 29 | 0x30, 0x39, 30 | 0x41, 0x5a, 31 | 0x5f, 0x5f, 32 | 0x61, 0x7a, 33 | }; 34 | 35 | static final HashMap PERL_GROUPS = 36 | new HashMap(); 37 | 38 | static { 39 | PERL_GROUPS.put("\\d", new CharGroup(+1, code1)); 40 | PERL_GROUPS.put("\\D", new CharGroup(-1, code1)); 41 | PERL_GROUPS.put("\\s", new CharGroup(+1, code2)); 42 | PERL_GROUPS.put("\\S", new CharGroup(-1, code2)); 43 | PERL_GROUPS.put("\\w", new CharGroup(+1, code3)); 44 | PERL_GROUPS.put("\\W", new CharGroup(-1, code3)); 45 | } 46 | private static final int[] code4 = { /* [:alnum:] */ 47 | 0x30, 0x39, 48 | 0x41, 0x5a, 49 | 0x61, 0x7a, 50 | }; 51 | 52 | private static final int[] code5 = { /* [:alpha:] */ 53 | 0x41, 0x5a, 54 | 0x61, 0x7a, 55 | }; 56 | 57 | private static final int[] code6 = { /* [:ascii:] */ 58 | 0x0, 0x7f, 59 | }; 60 | 61 | private static final int[] code7 = { /* [:blank:] */ 62 | 0x9, 0x9, 63 | 0x20, 0x20, 64 | }; 65 | 66 | private static final int[] code8 = { /* [:cntrl:] */ 67 | 0x0, 0x1f, 68 | 0x7f, 0x7f, 69 | }; 70 | 71 | private static final int[] code9 = { /* [:digit:] */ 72 | 0x30, 0x39, 73 | }; 74 | 75 | private static final int[] code10 = { /* [:graph:] */ 76 | 0x21, 0x7e, 77 | }; 78 | 79 | private static final int[] code11 = { /* [:lower:] */ 80 | 0x61, 0x7a, 81 | }; 82 | 83 | private static final int[] code12 = { /* [:print:] */ 84 | 0x20, 0x7e, 85 | }; 86 | 87 | private static final int[] code13 = { /* [:punct:] */ 88 | 0x21, 0x2f, 89 | 0x3a, 0x40, 90 | 0x5b, 0x60, 91 | 0x7b, 0x7e, 92 | }; 93 | 94 | private static final int[] code14 = { /* [:space:] */ 95 | 0x9, 0xd, 96 | 0x20, 0x20, 97 | }; 98 | 99 | private static final int[] code15 = { /* [:upper:] */ 100 | 0x41, 0x5a, 101 | }; 102 | 103 | private static final int[] code16 = { /* [:word:] */ 104 | 0x30, 0x39, 105 | 0x41, 0x5a, 106 | 0x5f, 0x5f, 107 | 0x61, 0x7a, 108 | }; 109 | 110 | private static final int[] code17 = { /* [:xdigit:] */ 111 | 0x30, 0x39, 112 | 0x41, 0x46, 113 | 0x61, 0x66, 114 | }; 115 | 116 | static final HashMap POSIX_GROUPS = 117 | new HashMap(); 118 | 119 | static { 120 | POSIX_GROUPS.put("[:alnum:]", new CharGroup(+1, code4)); 121 | POSIX_GROUPS.put("[:^alnum:]", new CharGroup(-1, code4)); 122 | POSIX_GROUPS.put("[:alpha:]", new CharGroup(+1, code5)); 123 | POSIX_GROUPS.put("[:^alpha:]", new CharGroup(-1, code5)); 124 | POSIX_GROUPS.put("[:ascii:]", new CharGroup(+1, code6)); 125 | POSIX_GROUPS.put("[:^ascii:]", new CharGroup(-1, code6)); 126 | POSIX_GROUPS.put("[:blank:]", new CharGroup(+1, code7)); 127 | POSIX_GROUPS.put("[:^blank:]", new CharGroup(-1, code7)); 128 | POSIX_GROUPS.put("[:cntrl:]", new CharGroup(+1, code8)); 129 | POSIX_GROUPS.put("[:^cntrl:]", new CharGroup(-1, code8)); 130 | POSIX_GROUPS.put("[:digit:]", new CharGroup(+1, code9)); 131 | POSIX_GROUPS.put("[:^digit:]", new CharGroup(-1, code9)); 132 | POSIX_GROUPS.put("[:graph:]", new CharGroup(+1, code10)); 133 | POSIX_GROUPS.put("[:^graph:]", new CharGroup(-1, code10)); 134 | POSIX_GROUPS.put("[:lower:]", new CharGroup(+1, code11)); 135 | POSIX_GROUPS.put("[:^lower:]", new CharGroup(-1, code11)); 136 | POSIX_GROUPS.put("[:print:]", new CharGroup(+1, code12)); 137 | POSIX_GROUPS.put("[:^print:]", new CharGroup(-1, code12)); 138 | POSIX_GROUPS.put("[:punct:]", new CharGroup(+1, code13)); 139 | POSIX_GROUPS.put("[:^punct:]", new CharGroup(-1, code13)); 140 | POSIX_GROUPS.put("[:space:]", new CharGroup(+1, code14)); 141 | POSIX_GROUPS.put("[:^space:]", new CharGroup(-1, code14)); 142 | POSIX_GROUPS.put("[:upper:]", new CharGroup(+1, code15)); 143 | POSIX_GROUPS.put("[:^upper:]", new CharGroup(-1, code15)); 144 | POSIX_GROUPS.put("[:word:]", new CharGroup(+1, code16)); 145 | POSIX_GROUPS.put("[:^word:]", new CharGroup(-1, code16)); 146 | POSIX_GROUPS.put("[:xdigit:]", new CharGroup(+1, code17)); 147 | POSIX_GROUPS.put("[:^xdigit:]", new CharGroup(-1, code17)); 148 | } 149 | 150 | } 151 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/re2j/DFAMachine.java: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The RE2 Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Original RE2 source here: 6 | // https://github.com/google/re2/blob/master/re2/dfa.cc 7 | 8 | package com.google.re2j; 9 | 10 | import com.google.re2j.RE2.Anchor; 11 | import com.google.re2j.RE2.MatchKind; 12 | 13 | import java.util.concurrent.ConcurrentHashMap; 14 | import java.util.concurrent.atomic.AtomicInteger; 15 | 16 | import static com.google.re2j.DFA.NO_MATCH; 17 | import static com.google.re2j.RE2.Anchor.ANCHOR_START; 18 | import static com.google.re2j.RE2.MatchKind.FIRST_MATCH; 19 | import static com.google.re2j.RE2.MatchKind.LONGEST_MATCH; 20 | 21 | /** 22 | * A {@link Machine} implementation using a DFA. 23 | */ 24 | class DFAMachine implements Machine { 25 | 26 | private static final int MAX_DFA_KEY = 4; 27 | 28 | @SuppressWarnings("unchecked") 29 | private final ConcurrentHashMap[] stateCache = new ConcurrentHashMap[MAX_DFA_KEY]; 30 | private final AtomicInteger availableStates; 31 | @SuppressWarnings("unchecked") 32 | private final ThreadLocal[] dfaCache = new ThreadLocal[MAX_DFA_KEY]; 33 | private final RE2 re2; 34 | 35 | DFAMachine(RE2 re2, int maximumNumberOfDFAStates) { 36 | this.re2 = re2; 37 | this.availableStates = new AtomicInteger(maximumNumberOfDFAStates); 38 | 39 | for (int i = 0; i < MAX_DFA_KEY; ++i) { 40 | stateCache[i] = new ConcurrentHashMap<>(); 41 | } 42 | 43 | setDfaThreadLocal(LONGEST_MATCH, true); 44 | setDfaThreadLocal(LONGEST_MATCH, false); 45 | setDfaThreadLocal(FIRST_MATCH, true); 46 | setDfaThreadLocal(FIRST_MATCH, false); 47 | } 48 | 49 | @Override 50 | public boolean match(MachineInput in, int pos, Anchor anchor, int[] submatches) { 51 | // Don't ask for the location if we won't use it. SearchDFA can do extra optimizations in that case. 52 | boolean wantMatchPosition = true; 53 | if (submatches.length == 0) { 54 | wantMatchPosition = false; 55 | } 56 | 57 | // Use DFA to find exact location of match, filter out non-matches. 58 | int matchStart; 59 | int matchEnd; 60 | switch (anchor) { 61 | case UNANCHORED: 62 | matchEnd = searchDFA(in, pos, in.endPos(), anchor, wantMatchPosition, re2.matchKind, false); 63 | if (matchEnd == NO_MATCH) { 64 | return false; 65 | } 66 | 67 | // Matched. Don't care where 68 | if (!wantMatchPosition) { 69 | return true; 70 | } 71 | 72 | // SearchDFA gives match end position but we don't know where the match started. Run the 73 | // regexp backwards from end position to find the longest possible match -- that's where it started. 74 | matchStart = searchDFA(in, pos, matchEnd, ANCHOR_START, true, LONGEST_MATCH, true); 75 | if (matchStart == NO_MATCH) { 76 | throw new IllegalStateException("reverse DFA did not found a match"); 77 | } 78 | 79 | break; 80 | case ANCHOR_BOTH: 81 | case ANCHOR_START: 82 | matchEnd = searchDFA(in, pos, in.endPos(), anchor, wantMatchPosition, re2.matchKind, false); 83 | if (matchEnd == NO_MATCH) { 84 | return false; 85 | } 86 | matchStart = 0; 87 | break; 88 | default: 89 | throw new IllegalStateException("bad anchor"); 90 | } 91 | 92 | if (submatches.length == 2) { 93 | submatches[0] = matchStart; 94 | submatches[1] = matchEnd; 95 | } else { 96 | if (!re2.nfaMachine.get().match(in, matchStart, anchor, submatches)) { 97 | throw new IllegalStateException("NFA inconsistency"); 98 | } 99 | } 100 | 101 | return true; 102 | } 103 | 104 | private int searchDFA(MachineInput in, int startPos, int endPos, Anchor anchor, boolean wantMatchPosition, MatchKind matchKind, boolean reversed) { 105 | boolean hasCarat = reversed ? anchor.isAnchorEnd() : anchor.isAnchorStart(); 106 | if (hasCarat && startPos != 0) { 107 | return NO_MATCH; 108 | } 109 | 110 | // Handle end match by running an anchored longest match and then checking if it covers all of text. 111 | boolean anchored = anchor.isAnchorStart(); 112 | boolean endMatch = false; 113 | if (anchor.isAnchorEnd()) { 114 | endMatch = true; 115 | matchKind = LONGEST_MATCH; 116 | } 117 | 118 | // If the caller doesn't care where the match is (just whether one exists), 119 | // then we can stop at the very first match we find, the so-called 120 | // "earliest match". 121 | boolean wantEarliestMatch = false; 122 | if (!wantMatchPosition && !endMatch) { 123 | wantEarliestMatch = true; 124 | matchKind = LONGEST_MATCH; 125 | } 126 | 127 | DFA dfa = getDfa(matchKind, reversed); 128 | int match = dfa.search(in, startPos, endPos, anchored, wantEarliestMatch); 129 | 130 | if (match == NO_MATCH) { 131 | return NO_MATCH; 132 | } 133 | 134 | if (endMatch) { 135 | if ((reversed && match != startPos) || (!reversed && match != endPos)) { 136 | return NO_MATCH; 137 | } 138 | } 139 | 140 | return match; 141 | } 142 | 143 | private DFA getDfa(MatchKind matchKind, boolean reversed) { 144 | return dfaCache[dfaKey(matchKind, reversed)].get(); 145 | } 146 | 147 | private int dfaKey(MatchKind matchKind, boolean reversed) { 148 | int longestInt = matchKind == LONGEST_MATCH ? 1 : 0; 149 | int reversedInt = reversed ? 1 : 0; 150 | return longestInt | (reversedInt << 1); 151 | } 152 | 153 | private void setDfaThreadLocal(final MatchKind matchKind, final boolean reversed) { 154 | final int dfaKey = dfaKey(matchKind, reversed); 155 | final Prog prog = reversed ? re2.reverseProg : re2.prog; 156 | dfaCache[dfaKey] = new ThreadLocal() { 157 | @Override 158 | public DFA initialValue() { 159 | return new DFA(prog, matchKind, reversed, stateCache[dfaKey], availableStates); 160 | } 161 | }; 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/re2j/DFAState.java: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The RE2 Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Original RE2 source here: 6 | // https://github.com/google/re2/blob/master/re2/dfa.cc 7 | 8 | package com.google.re2j; 9 | 10 | import static com.google.re2j.DFA.FLAG_MATCH; 11 | import static com.google.re2j.DFAState.StateType.DEAD; 12 | import static com.google.re2j.DFAState.StateType.REGULAR; 13 | import static java.lang.System.arraycopy; 14 | 15 | final class DFAState { 16 | public static final DFAState DEAD_STATE = new DFAState(DEAD); 17 | 18 | public enum StateType { 19 | DEAD, // no possible match out of this state 20 | REGULAR // all other states 21 | } 22 | 23 | private final StateType type; // the state type. Lets us create DEAD_STATE and FULL_MATCH_STATE 24 | private final int[] instIndexes; // indexes into prog instructions for this state 25 | private final int flag; // empty width flags 26 | private final DFAState[] next = new DFAState[256]; // Maps bytes to the next state to follow 27 | 28 | public DFAState(int[] instIndexes, int nIndexes, int flag) { 29 | this.type = REGULAR; 30 | this.instIndexes = new int[nIndexes]; 31 | arraycopy(instIndexes, 0, this.instIndexes, 0, nIndexes); 32 | this.flag = flag; 33 | } 34 | 35 | private DFAState(StateType type) { 36 | this.type = type; 37 | this.instIndexes = new int[0]; 38 | this.flag = 0; 39 | } 40 | 41 | public StateType getType() { 42 | return type; 43 | } 44 | 45 | public int getFlag() { 46 | return flag; 47 | } 48 | 49 | public int[] getInstIndexes() { 50 | return instIndexes; 51 | } 52 | 53 | public boolean isMatch() { 54 | return (flag & FLAG_MATCH) != 0; 55 | } 56 | 57 | public boolean isDead() { 58 | return type == DEAD; 59 | } 60 | 61 | public DFAState getNextState(byte b) { 62 | return next[b & 0xff]; 63 | } 64 | 65 | public void setNextState(byte b, DFAState state) { 66 | next[b & 0xff] = state; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/re2j/DFAStateKey.java: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The RE2 Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Original RE2 source here: 6 | // https://github.com/google/re2/blob/master/re2/dfa.cc 7 | 8 | package com.google.re2j; 9 | 10 | import java.util.Arrays; 11 | 12 | import static com.google.re2j.Utils.arrayFirstElementsEqual; 13 | 14 | final class DFAStateKey { 15 | private final int[] instIndexes; 16 | private final int nIndexes; 17 | private final int flag; 18 | 19 | DFAStateKey(int[] instIndexes, int nIndexes, int flag) { 20 | this.instIndexes = instIndexes; 21 | this.nIndexes = nIndexes; 22 | this.flag = flag; 23 | } 24 | 25 | @Override 26 | public boolean equals(Object o) { 27 | if (this == o) return true; 28 | if (o == null || getClass() != o.getClass()) return false; 29 | 30 | DFAStateKey that = (DFAStateKey) o; 31 | 32 | return nIndexes == that.nIndexes && flag == that.flag && arrayFirstElementsEqual(instIndexes, that.instIndexes, nIndexes); 33 | } 34 | 35 | @Override 36 | public int hashCode() { 37 | int result = Arrays.hashCode(instIndexes); 38 | result = 31 * result + nIndexes; 39 | result = 31 * result + flag; 40 | return result; 41 | } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/com/google/re2j/Inst.java: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Original Go source here: 6 | // http://code.google.com/p/go/source/browse/src/pkg/regexp/syntax/prog.go 7 | 8 | package com.google.re2j; 9 | 10 | import static com.google.re2j.Inst.Op.BYTE; 11 | 12 | /** 13 | * A single instruction in the regular expression virtual machine. 14 | * 15 | * @see http://swtch.com/~rsc/regexp/regexp2.html 16 | */ 17 | class Inst { 18 | 19 | enum Op { 20 | ALT, 21 | ALT_MATCH, 22 | CAPTURE, 23 | EMPTY_WIDTH, 24 | FAIL, 25 | MATCH, 26 | NOP, 27 | BYTE, 28 | BYTE1 29 | } 30 | 31 | Op op; 32 | int out; // all but MATCH, FAIL 33 | int arg; // ALT, ALT_MATCH, CAPTURE, EMPTY_WIDTH 34 | byte[] byteRanges; // length==1 => exact match. Otherwise a list of [lo,hi] pairs. hi is *inclusive*. 35 | 36 | Inst(Op op) { 37 | this.op = op; 38 | } 39 | 40 | // op() returns i.Op but merges all the byte special cases into BYTE 41 | // Beware "op" is a public field. 42 | Op op() { 43 | switch (op) { 44 | case BYTE1: 45 | return BYTE; 46 | default: 47 | return op; 48 | } 49 | } 50 | 51 | // MatchByte returns true if the instruction matches (and consumes) b. 52 | // It should only be called when op == InstByte. 53 | boolean matchByte(byte b) { 54 | // Special case: single-byte slice is from literal string, not byte range. 55 | if (byteRanges.length == 1) { 56 | int b0 = byteRanges[0]; 57 | return b == b0; 58 | } 59 | 60 | // Search through all pairs. 61 | int byteInt = b & 0xff; 62 | for (int j = 0; j < byteRanges.length; j += 2) { 63 | if (byteInt < (byteRanges[j] & 0xff)) { 64 | return false; 65 | } 66 | if (byteInt <= (byteRanges[j + 1] & 0xff)) { 67 | return true; 68 | } 69 | } 70 | 71 | return false; 72 | } 73 | 74 | @Override 75 | public String toString() { 76 | switch (op) { 77 | case ALT: 78 | return "alt -> " + out + ", " + arg; 79 | case ALT_MATCH: 80 | return "altmatch -> " + out + ", " + arg; 81 | case CAPTURE: 82 | return "cap " + arg + " -> " + out; 83 | case EMPTY_WIDTH: 84 | return "empty " + arg + " -> " + out; 85 | case MATCH: 86 | return "match"; 87 | case FAIL: 88 | return "fail"; 89 | case NOP: 90 | return "nop -> " + out; 91 | case BYTE: 92 | return "byte " + appendBytes() + " -> " + out; 93 | case BYTE1: 94 | return "byte1 " + appendBytes() + " -> " + out; 95 | default: 96 | throw new IllegalStateException("unhandled case in Inst.toString"); 97 | } 98 | } 99 | 100 | private String appendBytes() { 101 | StringBuilder out = new StringBuilder(); 102 | if (byteRanges.length == 1) { 103 | out.append(byteRanges[0] & 0xff); 104 | } else { 105 | for (int i = 0; i < byteRanges.length; i += 2) { 106 | out.append("[") 107 | .append(byteRanges[i] & 0xff) 108 | .append(",") 109 | .append(byteRanges[i + 1] & 0xff) 110 | .append("]"); 111 | if (i < byteRanges.length - 2) { 112 | out.append(";"); 113 | } 114 | } 115 | } 116 | return out.toString(); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/re2j/Machine.java: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Original Go source here: 6 | // http://code.google.com/p/go/source/browse/src/pkg/regexp/exec.go 7 | 8 | package com.google.re2j; 9 | 10 | import com.google.re2j.RE2.Anchor; 11 | 12 | /** 13 | * A Machine matches an input string of Unicode characters against an RE2 instance. 14 | */ 15 | interface Machine { 16 | 17 | /** 18 | * Runs the machine over the input |in| starting at |pos| with the RE2 Anchor |anchor|. 19 | * |submatches| contains group positions after a successful match. 20 | * 21 | * @return reports whether a match was found. 22 | */ 23 | boolean match(MachineInput in, int pos, Anchor anchor, int[] submatches); 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/re2j/MachineInput.java: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Original Go source here: 6 | // http://code.google.com/p/go/source/browse/src/pkg/regexp/regexp.go 7 | 8 | package com.google.re2j; 9 | 10 | import com.livejournal.karino2.zipsourcecodereading.Slice; 11 | 12 | 13 | /** 14 | * MachineInput represents the UTF-8 input text supplied to the Machine. It provides one-character 15 | * lookahead. 16 | */ 17 | final class MachineInput { 18 | 19 | 20 | static final byte EOF = -1; 21 | 22 | static MachineInput fromUTF8(Slice slice) { 23 | return new MachineInput(slice); 24 | } 25 | 26 | final Slice slice; 27 | /* 28 | final Object base; 29 | final long address; 30 | */ 31 | final int length; 32 | 33 | MachineInput(Slice slice) { 34 | this.slice = slice; 35 | /* 36 | this.base = slice.getBase(); 37 | this.address = slice.getAddress(); 38 | */ 39 | this.length = slice.length(); 40 | } 41 | 42 | // Returns the byte at the specified index. 43 | byte getByte(int i) { 44 | if (i >= length) { 45 | return EOF; 46 | } 47 | 48 | if (i < 0) { 49 | throw new IndexOutOfBoundsException("index less than zero (" + i + ")"); 50 | } 51 | 52 | return getByteUnchecked(i); 53 | } 54 | 55 | byte getByteUnchecked(int i) { 56 | return slice.getByte(i); 57 | } 58 | 59 | // Returns the index relative to |pos| at which |re2.prefix| is found 60 | // in this input stream, or a negative value if not found. 61 | int index(RE2 re2, int pos) { 62 | int i = Utils.indexOf(slice, re2.prefixUTF8, pos); 63 | return i < 0 ? i : i - pos; 64 | } 65 | 66 | // Returns the end position in the same units as step(). 67 | int endPos() { 68 | return length; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/re2j/Options.java: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Teradata. All Rights Reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package com.google.re2j; 6 | 7 | import java.io.Serializable; 8 | 9 | import static com.google.re2j.Options.Algorithm.DFA; 10 | import static java.util.Objects.requireNonNull; 11 | 12 | public final class Options implements Serializable { 13 | public static final Options DEFAULT_OPTIONS = builder().build(); 14 | 15 | // Start state + end state 16 | private static final int MINIMUM_NUMBER_OF_DFA_STATES = 2; 17 | private static final int DEFAULT_NUMBER_OF_DFA_RETRIES = 5; 18 | 19 | private Algorithm algorithm = DFA; 20 | private EventsListener eventsListener = null; 21 | private int maximumNumberOfDFAStates = Integer.MAX_VALUE; 22 | private int numberOfDFARetries = DEFAULT_NUMBER_OF_DFA_RETRIES; 23 | 24 | public enum Algorithm { 25 | // Use DFA exclusively, throw an exception when maximum number of DFA states is reached n times. 26 | // DFA machine is reset each time states cache is full. 27 | DFA, 28 | // Use DFA, fallback to NFA when maximum number of DFA states is reached n times. DFA machine 29 | // is reset each time states cache is full. 30 | DFA_FALLBACK_TO_NFA, 31 | // use NFA exclusively 32 | NFA 33 | } 34 | 35 | public Algorithm getAlgorithm() { 36 | return algorithm; 37 | } 38 | 39 | public EventsListener getEventsListener() { 40 | return eventsListener; 41 | } 42 | 43 | public int getMaximumNumberOfDFAStates() { 44 | return maximumNumberOfDFAStates; 45 | } 46 | 47 | public int getNumberOfDFARetries() { 48 | return numberOfDFARetries; 49 | } 50 | 51 | @Override 52 | public boolean equals(Object o) { 53 | if (this == o) return true; 54 | if (o == null || getClass() != o.getClass()) return false; 55 | 56 | Options options = (Options) o; 57 | 58 | return maximumNumberOfDFAStates == options.maximumNumberOfDFAStates 59 | && numberOfDFARetries == options.numberOfDFARetries 60 | && algorithm == options.algorithm 61 | && !(eventsListener != null ? !eventsListener.equals(options.eventsListener) : options.eventsListener != null); 62 | 63 | } 64 | 65 | @Override 66 | public int hashCode() { 67 | int result = algorithm.hashCode(); 68 | result = 31 * result + (eventsListener != null ? eventsListener.hashCode() : 0); 69 | result = 31 * result + maximumNumberOfDFAStates; 70 | result = 31 * result + numberOfDFARetries; 71 | return result; 72 | } 73 | 74 | @Override 75 | public String toString() { 76 | return "Options{" + 77 | "algorithm=" + algorithm + 78 | ", eventsListener=" + eventsListener + 79 | ", maximumNumberOfDFAStates=" + maximumNumberOfDFAStates + 80 | ", numberOfDFARetries=" + numberOfDFARetries + 81 | '}'; 82 | } 83 | 84 | public static OptionsBuilder builder() { 85 | return new OptionsBuilder(); 86 | } 87 | 88 | /** 89 | * Interface for RE2J events listening. 90 | */ 91 | public interface EventsListener { 92 | 93 | /** 94 | * Called when NFA is being used instead of DFA because too many {@link DFAState}s has been 95 | * created. 96 | */ 97 | void fallbackToNFA(); 98 | } 99 | 100 | public static final class OptionsBuilder { 101 | private Options options = new Options(); 102 | 103 | public OptionsBuilder setAlgorithm(Algorithm algorithm) { 104 | options.algorithm = requireNonNull(algorithm); 105 | return this; 106 | } 107 | 108 | public OptionsBuilder setMaximumNumberOfDFAStates(int maximumNumberOfDFAStates) { 109 | if (maximumNumberOfDFAStates < MINIMUM_NUMBER_OF_DFA_STATES) { 110 | throw new IllegalArgumentException("maximum number of DFA states must be larger or equal to " + MINIMUM_NUMBER_OF_DFA_STATES); 111 | } 112 | options.maximumNumberOfDFAStates = maximumNumberOfDFAStates; 113 | return this; 114 | } 115 | 116 | public OptionsBuilder setNumberOfDFARetries(int numberOfDFARetries) { 117 | if (numberOfDFARetries < 0) { 118 | throw new IllegalArgumentException("number of DFA retries cannot be below 0"); 119 | } 120 | options.numberOfDFARetries = numberOfDFARetries; 121 | return this; 122 | } 123 | 124 | public OptionsBuilder setEventsListener(EventsListener eventsListener) { 125 | options.eventsListener = requireNonNull(eventsListener); 126 | return this; 127 | } 128 | 129 | public Options build() { 130 | return options; 131 | } 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/re2j/PatternSyntaxException.java: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package com.google.re2j; 6 | 7 | /** 8 | * An exception thrown by the parser if the pattern was invalid. 9 | * 10 | * Following {@code java.util.regex.PatternSyntaxException}, this is an 11 | * unchecked exception. 12 | */ 13 | public class PatternSyntaxException extends RuntimeException { 14 | 15 | private final String error; // the nature of the error 16 | private final String input; // the partial input at the point of error. 17 | 18 | public PatternSyntaxException(String error, String input) { 19 | super("error parsing regexp: " + error + ": `" + input + "`"); 20 | this.error = error; 21 | this.input = input; 22 | } 23 | 24 | public PatternSyntaxException(String error) { 25 | super("error parsing regexp: " + error); 26 | this.error = error; 27 | this.input = ""; 28 | } 29 | 30 | /** 31 | * Retrieves the error index. 32 | * 33 | * @return The approximate index in the pattern of the error, 34 | * or -1 if the index is not known 35 | */ 36 | public int getIndex() { 37 | return -1; 38 | } 39 | 40 | /** 41 | * Retrieves the description of the error. 42 | * 43 | * @return The description of the error 44 | */ 45 | public String getDescription() { 46 | return error; 47 | } 48 | 49 | /** 50 | * Retrieves the erroneous regular-expression pattern. 51 | * 52 | * @return The erroneous pattern 53 | */ 54 | public String getPattern() { 55 | return input; 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/re2j/Prog.java: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Original Go source here: 6 | // http://code.google.com/p/go/source/browse/src/pkg/regexp/syntax/prog.go 7 | 8 | package com.google.re2j; 9 | 10 | import java.io.ByteArrayOutputStream; 11 | import java.io.IOException; 12 | import java.util.ArrayList; 13 | import java.util.List; 14 | 15 | import static com.google.re2j.Inst.Op.BYTE; 16 | import static com.google.re2j.Inst.Op.MATCH; 17 | 18 | /** 19 | * A Prog is a compiled regular expression program. 20 | */ 21 | class Prog { 22 | 23 | private final List inst = new ArrayList(); 24 | int start; // index of start instruction 25 | int startUnanchored; // index of unanchored start instruction 26 | int numCap = 2; // number of CAPTURE insts in re 27 | // 2 => implicit ( and ) for whole match $0 28 | 29 | // Constructs an empty program. 30 | Prog() {} 31 | 32 | // Returns the instruction at the specified pc. 33 | // Precondition: pc > 0 && pc < numInst(). 34 | Inst getInst(int pc) { 35 | return inst.get(pc); 36 | } 37 | 38 | Inst[] getInst() { 39 | return inst.toArray(new Inst[inst.size()]); 40 | } 41 | 42 | // Returns the number of instructions in this program. 43 | int numInst() { 44 | return inst.size(); 45 | } 46 | 47 | // Adds a new instruction to this program, with operator |op| and |pc| equal 48 | // to |numInst()|. 49 | void addInst(Inst.Op op) { 50 | inst.add(new Inst(op)); 51 | } 52 | 53 | // skipNop() follows any no-op or capturing instructions and returns the 54 | // resulting instruction. 55 | Inst skipNop(int pc) { 56 | Inst i = inst.get(pc); 57 | while (i.op == Inst.Op.NOP || i.op == Inst.Op.CAPTURE) { 58 | i = inst.get(pc); 59 | pc = i.out; 60 | } 61 | return i; 62 | } 63 | 64 | // prefix() returns a pair of a literal slice that all matches for the 65 | // regexp must start with, and a boolean which is true if the prefix is the 66 | // entire match. The slice is returned by appending to |prefix|. 67 | boolean prefix(ByteArrayOutputStream prefix) { 68 | Inst i = skipNop(start); 69 | 70 | // Avoid allocation of buffer if prefix is empty. 71 | if (i.op() != BYTE || i.byteRanges.length != 1) { 72 | return i.op == MATCH; // (append "" to prefix) 73 | } 74 | 75 | int length = 0; 76 | while (i.op() == BYTE && i.byteRanges.length == 1) { 77 | i = skipNop(i.out); 78 | length++; 79 | } 80 | 81 | byte[] bytes = new byte[length]; 82 | length = 0; 83 | i = skipNop(start); 84 | while (i.op() == BYTE && i.byteRanges.length == 1) { 85 | bytes[length] = i.byteRanges[0]; 86 | i = skipNop(i.out); 87 | length++; 88 | } 89 | 90 | // Have prefix; gather characters. 91 | try { 92 | prefix.write(bytes); 93 | } catch (IOException e) { 94 | throw new RuntimeException("Never reached here."); 95 | } 96 | 97 | return i.op == MATCH; 98 | } 99 | 100 | // startCond() returns the leading empty-width conditions that must be true 101 | // in any match. It returns -1 (all bits set) if no matches are possible. 102 | int startCond() { 103 | int flag = 0; // bitmask of EMPTY_* flags 104 | int pc = start; 105 | loop: 106 | for (;;) { 107 | Inst i = inst.get(pc); 108 | switch (i.op) { 109 | case EMPTY_WIDTH: 110 | flag |= i.arg; 111 | break; 112 | case FAIL: 113 | return -1; 114 | case CAPTURE: 115 | case NOP: 116 | break; // skip 117 | default: 118 | break loop; 119 | } 120 | pc = i.out; 121 | } 122 | return flag; 123 | } 124 | 125 | // --- Patch list --- 126 | 127 | // A patchlist is a list of instruction pointers that need to be filled in 128 | // (patched). Because the pointers haven't been filled in yet, we can reuse 129 | // their storage to hold the list. It's kind of sleazy, but works well in 130 | // practice. See http://swtch.com/~rsc/regexp/regexp1.html for inspiration. 131 | 132 | // These aren't really pointers: they're integers, so we can reinterpret them 133 | // this way without using package unsafe. A value l denotes p.inst[l>>1].out 134 | // (l&1==0) or .arg (l&1==1). l == 0 denotes the empty list, okay because we 135 | // start every program with a fail instruction, so we'll never want to point 136 | // at its output link. 137 | 138 | int next(int l) { 139 | Inst i = inst.get(l >> 1); 140 | if ((l & 1) == 0) { 141 | return i.out; 142 | } 143 | return i.arg; 144 | } 145 | 146 | void patch(int l, int val) { 147 | while (l != 0) { 148 | Inst i = inst.get(l >> 1); 149 | if ((l & 1) == 0) { 150 | l = i.out; 151 | i.out = val; 152 | } else { 153 | l = i.arg; 154 | i.arg = val; 155 | } 156 | } 157 | } 158 | 159 | int append(int l1, int l2) { 160 | if (l1 == 0) { 161 | return l2; 162 | } 163 | if (l2 == 0) { 164 | return l1; 165 | } 166 | int last = l1; 167 | for (;;) { 168 | int next = next(last); 169 | if (next == 0) { 170 | break; 171 | } 172 | last = next; 173 | } 174 | Inst i = inst.get(last>>1); 175 | if ((last & 1) == 0) { 176 | i.out = l2; 177 | } else { 178 | i.arg = l2; 179 | } 180 | return l1; 181 | } 182 | 183 | // --- 184 | 185 | @Override 186 | public String toString() { 187 | StringBuilder out = new StringBuilder(); 188 | for (int pc = 0; pc < inst.size(); ++pc) { 189 | int len = out.length(); 190 | out.append(pc); 191 | if (pc == start) { 192 | out.append('*'); 193 | } 194 | if (pc == startUnanchored) { 195 | out.append("@"); 196 | } 197 | // Use spaces not tabs since they're not always preserved in 198 | // Google Java source, such as our tests. 199 | out.append(" ".substring(out.length() - len)). 200 | append(inst.get(pc)).append('\n'); 201 | } 202 | return out.toString(); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/re2j/Simplify.java: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Original Go source here: 6 | // http://code.google.com/p/go/source/browse/src/pkg/regexp/syntax/simplify.go 7 | 8 | package com.google.re2j; 9 | 10 | import java.util.ArrayList; 11 | 12 | class Simplify { 13 | 14 | // Simplify returns a regexp equivalent to re but without counted 15 | // repetitions and with various other simplifications, such as 16 | // rewriting /(?:a+)+/ to /a+/. The resulting regexp will execute 17 | // correctly but its string representation will not produce the same 18 | // parse tree, because capturing parentheses may have been duplicated 19 | // or removed. For example, the simplified form for /(x){1,2}/ is 20 | // /(x)(x)?/ but both parentheses capture as $1. The returned regexp 21 | // may share structure with or be the original. 22 | static Regexp simplify(Regexp re) { 23 | if (re == null) { 24 | return null; 25 | } 26 | switch (re.op) { 27 | case CAPTURE: 28 | case CONCAT: 29 | case ALTERNATE: { 30 | // Simplify children, building new Regexp if children change. 31 | Regexp nre = re; 32 | for (int i = 0; i < re.subs.length; ++i) { 33 | Regexp sub = re.subs[i]; 34 | Regexp nsub = simplify(sub); 35 | if (nre == re && nsub != sub) { 36 | // Start a copy. 37 | nre = new Regexp(re); // shallow copy 38 | nre.runes = null; 39 | nre.subs = Parser.subarray(re.subs, 0, re.subs.length); // clone 40 | } 41 | if (nre != re) { 42 | nre.subs[i] = nsub; 43 | } 44 | } 45 | return nre; 46 | } 47 | case STAR: 48 | case PLUS: 49 | case QUEST: { 50 | Regexp sub = simplify(re.subs[0]); 51 | return simplify1(re.op, re.flags, sub, re); 52 | } 53 | case REPEAT: { 54 | // Special special case: x{0} matches the empty string 55 | // and doesn't even need to consider x. 56 | if (re.min == 0 && re.max == 0) { 57 | return new Regexp(Regexp.Op.EMPTY_MATCH); 58 | } 59 | 60 | // The fun begins. 61 | Regexp sub = simplify(re.subs[0]); 62 | 63 | // x{n,} means at least n matches of x. 64 | if (re.max == -1) { 65 | // Special case: x{0,} is x*. 66 | if (re.min == 0) { 67 | return simplify1(Regexp.Op.STAR, re.flags, sub, null); 68 | } 69 | 70 | // Special case: x{1,} is x+. 71 | if (re.min == 1) { 72 | return simplify1(Regexp.Op.PLUS, re.flags, sub, null); 73 | } 74 | 75 | // General case: x{4,} is xxxx+. 76 | Regexp nre = new Regexp(Regexp.Op.CONCAT); 77 | ArrayList subs = new ArrayList(); 78 | for (int i = 0; i < re.min - 1; i++) { 79 | subs.add(sub); 80 | } 81 | subs.add(simplify1(Regexp.Op.PLUS, re.flags, sub, null)); 82 | nre.subs = subs.toArray(new Regexp[subs.size()]); 83 | return nre; 84 | } 85 | 86 | // Special case x{0} handled above. 87 | 88 | // Special case: x{1} is just x. 89 | if (re.min == 1 && re.max == 1) { 90 | return sub; 91 | } 92 | 93 | // General case: x{n,m} means n copies of x and m copies of x? 94 | // The machine will do less work if we nest the final m copies, 95 | // so that x{2,5} = xx(x(x(x)?)?)? 96 | 97 | // Build leading prefix: xx. 98 | ArrayList prefixSubs = null; 99 | if (re.min > 0) { 100 | prefixSubs = new ArrayList(); 101 | for (int i = 0; i < re.min; i++) { 102 | prefixSubs.add(sub); 103 | } 104 | } 105 | 106 | // Build and attach suffix: (x(x(x)?)?)? 107 | if (re.max > re.min) { 108 | Regexp suffix = simplify1(Regexp.Op.QUEST, re.flags, sub, null); 109 | for (int i = re.min + 1; i < re.max; i++) { 110 | Regexp nre2 = new Regexp(Regexp.Op.CONCAT); 111 | nre2.subs = new Regexp[] { sub, suffix }; 112 | suffix = simplify1(Regexp.Op.QUEST, re.flags, nre2, null); 113 | } 114 | if (prefixSubs == null) { 115 | return suffix; 116 | } 117 | prefixSubs.add(suffix); 118 | } 119 | if (prefixSubs != null) { 120 | Regexp prefix = new Regexp(Regexp.Op.CONCAT); 121 | prefix.subs = prefixSubs.toArray(new Regexp[prefixSubs.size()]); 122 | return prefix; 123 | } 124 | 125 | // Some degenerate case like min > max or min < max < 0. 126 | // Handle as impossible match. 127 | return new Regexp(Regexp.Op.NO_MATCH); 128 | } 129 | } 130 | 131 | return re; 132 | } 133 | 134 | // simplify1 implements Simplify for the unary OpStar, 135 | // OpPlus, and OpQuest operators. It returns the simple regexp 136 | // equivalent to 137 | // 138 | // Regexp{Op: op, Flags: flags, Sub: {sub}} 139 | // 140 | // under the assumption that sub is already simple, and 141 | // without first allocating that structure. If the regexp 142 | // to be returned turns out to be equivalent to re, simplify1 143 | // returns re instead. 144 | // 145 | // simplify1 is factored out of Simplify because the implementation 146 | // for other operators generates these unary expressions. 147 | // Letting them call simplify1 makes sure the expressions they 148 | // generate are simple. 149 | private static Regexp simplify1(Regexp.Op op, int flags, Regexp sub, 150 | Regexp re) { 151 | // Special case: repeat the empty string as much as 152 | // you want, but it's still the empty string. 153 | if (sub.op == Regexp.Op.EMPTY_MATCH) { 154 | return sub; 155 | } 156 | // The operators are idempotent if the flags match. 157 | if (op == sub.op && 158 | (flags & RE2.NON_GREEDY) == (sub.flags & RE2.NON_GREEDY)) { 159 | return sub; 160 | } 161 | if (re != null && re.op == op && 162 | (re.flags & RE2.NON_GREEDY) == (flags & RE2.NON_GREEDY) && 163 | sub == re.subs[0]) { 164 | return re; 165 | } 166 | 167 | re = new Regexp(op); 168 | re.flags = flags; 169 | re.subs = new Regexp[] { sub }; 170 | return re; 171 | } 172 | 173 | private Simplify() {} // uninstantiable 174 | 175 | } 176 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/re2j/SliceUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | package com.google.re2j; 15 | 16 | 17 | /** 18 | * Utility methods related to {@link Slice} class. 19 | */ 20 | final class SliceUtils { 21 | 22 | /* 23 | static void appendReplacement(SliceOutput so, Slice replacement, Matcher matcher) { 24 | int idx = 0; 25 | 26 | // Handle the following items: 27 | // 1. ${name}; 28 | // 2. $0, $1, $123 (group 123, if exists; or group 12, if exists; or group 1); 29 | // 3. \\, \$, \t (literal 't'). 30 | // 4. Anything that doesn't starts with \ or $ is considered regular bytes 31 | while (idx < replacement.length()) { 32 | byte nextByte = replacement.getByte(idx); 33 | if (nextByte == '$') { 34 | idx++; 35 | if (idx == replacement.length()) { 36 | throw new IllegalArgumentException("Illegal replacement sequence: " + replacement.toStringUtf8()); 37 | } 38 | nextByte = replacement.getByte(idx); 39 | int backref; 40 | if (nextByte == '{') { // case 1 in the above comment 41 | idx++; 42 | int startCursor = idx; 43 | while (idx < replacement.length()) { 44 | nextByte = replacement.getByte(idx); 45 | if (nextByte == '}') { 46 | break; 47 | } 48 | idx++; 49 | } 50 | String groupName = replacement.slice(startCursor, idx - startCursor).toStringUtf8(); 51 | Integer namedGroupIndex = matcher.pattern().re2().namedGroupIndexes.get(groupName); 52 | if (namedGroupIndex == null) { 53 | throw new IndexOutOfBoundsException("Illegal replacement sequence: unknown group " + groupName); 54 | } 55 | backref = namedGroupIndex; 56 | idx++; 57 | } else { // case 2 in the above comment 58 | backref = nextByte - '0'; 59 | if (backref < 0 || backref > 9) { 60 | throw new IllegalArgumentException("Illegal replacement sequence: " + replacement.toStringUtf8()); 61 | } 62 | if (matcher.groupCount() < backref) { 63 | throw new IndexOutOfBoundsException("Illegal replacement sequence: unknown group " + backref); 64 | } 65 | idx++; 66 | while (idx < replacement.length()) { // Adaptive group number: find largest group num that is not greater than actual number of groups 67 | int nextDigit = replacement.getByte(idx) - '0'; 68 | if (nextDigit < 0 || nextDigit > 9) { 69 | break; 70 | } 71 | int newBackref = (backref * 10) + nextDigit; 72 | if (matcher.groupCount() < newBackref) { 73 | break; 74 | } 75 | backref = newBackref; 76 | idx++; 77 | } 78 | } 79 | Slice group = matcher.group(backref); 80 | if (group != null) { 81 | so.writeBytes(group); 82 | } 83 | } else { // case 3 and 4 in the above comment 84 | if (nextByte == '\\') { 85 | idx++; 86 | if (idx == replacement.length()) { 87 | throw new IllegalArgumentException("Illegal replacement sequence: " + replacement.toStringUtf8()); 88 | } 89 | nextByte = replacement.getByte(idx); 90 | } 91 | so.appendByte(nextByte); 92 | idx++; 93 | } 94 | } 95 | } 96 | */ 97 | private SliceUtils() { 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/re2j/SparseSet.java: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The RE2 Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Original RE2 source here: 6 | // https://github.com/google/re2/blob/master/util/sparse_set.h 7 | 8 | package com.google.re2j; 9 | 10 | class SparseSet { 11 | private final int[] dense; // may contain stale Entries in slots >= size 12 | private final int[] sparse; // may contain stale but in-bounds values. 13 | private int size; // of prefix of |dense| that is logically populated 14 | 15 | SparseSet(int n) { 16 | this.sparse = new int[n]; 17 | this.dense = new int[n]; 18 | } 19 | 20 | boolean contains(int i) { 21 | return sparse[i] < size && dense[sparse[i]] == i; 22 | } 23 | 24 | boolean isEmpty() { 25 | return size == 0; 26 | } 27 | 28 | void add(int i) { 29 | dense[size] = i; 30 | sparse[i] = size; 31 | size++; 32 | } 33 | 34 | void clear() { 35 | size = 0; 36 | } 37 | 38 | int getValueAt(int i) { 39 | if (i >= size) { 40 | throw new IndexOutOfBoundsException(String.format("Cannot get index %d. SparseSet is size %d", i, size)); 41 | } 42 | return dense[i]; 43 | } 44 | 45 | int getSize() { 46 | return size; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/re2j/Unicode.java: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Many of these were derived from the corresponding Go functions in 6 | // http://code.google.com/p/go/source/browse/src/pkg/unicode/letter.go 7 | 8 | package com.google.re2j; 9 | 10 | import static java.nio.charset.StandardCharsets.UTF_8; 11 | 12 | /** 13 | * Utilities for dealing with Unicode better than Java does. 14 | * 15 | * @author adonovan@google.com (Alan Donovan) 16 | */ 17 | public class Unicode { 18 | 19 | // Rune and UTF8 sequences are the same. 20 | static final int RUNE_SELF = 0x80; 21 | 22 | // The highest legal rune value. 23 | static final int MAX_RUNE = 0x10FFFF; 24 | 25 | // The highest legal ASCII value. 26 | static final int MAX_ASCII = 0x7f; 27 | 28 | // The highest legal Latin-1 value. 29 | static final int MAX_LATIN1 = 0xFF; 30 | 31 | private static final int MAX_CASE = 3; 32 | 33 | // Represents invalid code points. 34 | private static final int REPLACEMENT_CHAR = 0xFFFD; 35 | 36 | // Minimum and maximum runes involved in folding. 37 | // Checked during test. 38 | static final int MIN_FOLD = 0x0041; 39 | static final int MAX_FOLD = 0x1044f; 40 | 41 | // Maximum bytes per rune 42 | static final int UTF_MAX = 4; 43 | 44 | // is32 uses binary search to test whether rune is in the specified 45 | // slice of 32-bit ranges. 46 | // TODO(adonovan): opt: consider using int[n*3] instead of int[n][3]. 47 | private static boolean is32(int[][] ranges, int r) { 48 | // binary search over ranges 49 | for (int lo = 0, hi = ranges.length; lo < hi; ) { 50 | int m = lo + (hi - lo) / 2; 51 | int[] range = ranges[m]; // [lo, hi, stride] 52 | if (range[0] <= r && r <= range[1]) { 53 | return ((r - range[0]) % range[2]) == 0; 54 | } 55 | if (r < range[0]) { 56 | hi = m; 57 | } else { 58 | lo = m + 1; 59 | } 60 | } 61 | return false; 62 | } 63 | 64 | // is tests whether rune is in the specified table of ranges. 65 | private static boolean is(int[][] ranges, int r) { 66 | // common case: rune is ASCII or Latin-1, so use linear search. 67 | if (r <= MAX_LATIN1) { 68 | for (int[] range : ranges) { // range = [lo, hi, stride] 69 | if (r > range[1]) { 70 | continue; 71 | } 72 | if (r < range[0]) { 73 | return false; 74 | } 75 | return ((r - range[0]) % range[2]) == 0; 76 | } 77 | return false; 78 | } 79 | return ranges.length > 0 && 80 | r >= ranges[0][0] && 81 | is32(ranges, r); 82 | } 83 | 84 | static byte[] codePointToUtf8(int codePoint) { 85 | return new String(Character.toChars(codePoint)).getBytes(UTF_8); 86 | } 87 | 88 | static int maxRune(int len) { 89 | int b; // number of Rune bits in len-byte UTF-8 sequence (len < UTFmax) 90 | if (len == 1) { 91 | b = 7; 92 | } else { 93 | b = 8 - (len + 1) + 6 * (len - 1); 94 | } 95 | return (1 << b) - 1; // maximum Rune for b bits. 96 | } 97 | 98 | // isUpper reports whether the rune is an upper case letter. 99 | static boolean isUpper(int r) { 100 | // See comment in isGraphic. 101 | if (r <= MAX_LATIN1) { 102 | return Character.isUpperCase((char) r); 103 | } 104 | return is(UnicodeTables.Upper, r); 105 | } 106 | 107 | // isLower reports whether the rune is a lower case letter. 108 | static boolean isLower(int r) { 109 | // See comment in isGraphic. 110 | if (r <= MAX_LATIN1) { 111 | return Character.isLowerCase((char) r); 112 | } 113 | return is(UnicodeTables.Lower, r); 114 | } 115 | 116 | // isTitle reports whether the rune is a title case letter. 117 | static boolean isTitle(int r) { 118 | if (r <= MAX_LATIN1) { 119 | return false; 120 | } 121 | return is(UnicodeTables.Title, r); 122 | } 123 | 124 | // isPrint reports whether the rune is printable (Unicode L/M/N/P/S or ' '). 125 | static boolean isPrint(int r) { 126 | if (r <= MAX_LATIN1) { 127 | return r >= 0x20 && r < 0x7F || 128 | r >= 0xA1 && r != 0xAD; 129 | } 130 | return is(UnicodeTables.L, r) || 131 | is(UnicodeTables.M, r) || 132 | is(UnicodeTables.N, r) || 133 | is(UnicodeTables.P, r) || 134 | is(UnicodeTables.S, r); 135 | } 136 | 137 | // A case range is conceptually a record: 138 | // class CaseRange { 139 | // int lo, hi; 140 | // int upper, lower, title; 141 | // } 142 | // but flattened as an int[5]. 143 | 144 | // to maps the rune using the specified case mapping. 145 | private static int to(int kase, int r, int[][] caseRange) { 146 | if (kase < 0 || MAX_CASE <= kase) { 147 | return REPLACEMENT_CHAR; // as reasonable an error as any 148 | } 149 | // binary search over ranges 150 | for (int lo = 0, hi = caseRange.length; lo < hi; ) { 151 | int m = lo + (hi - lo) / 2; 152 | int[] cr = caseRange[m]; // cr = [lo, hi, upper, lower, title] 153 | int crlo = cr[0]; 154 | int crhi = cr[1]; 155 | if (crlo <= r && r <= crhi) { 156 | int delta = cr[2 + kase]; 157 | if (delta > MAX_RUNE) { 158 | // In an Upper-Lower sequence, which always starts with 159 | // an UpperCase letter, the real deltas always look like: 160 | // {0, 1, 0} UpperCase (Lower is next) 161 | // {-1, 0, -1} LowerCase (Upper, Title are previous) 162 | // The characters at even offsets from the beginning of the 163 | // sequence are upper case; the ones at odd offsets are lower. 164 | // The correct mapping can be done by clearing or setting the low 165 | // bit in the sequence offset. 166 | // The constants UpperCase and TitleCase are even while LowerCase 167 | // is odd so we take the low bit from kase. 168 | return crlo + (((r - crlo) & ~1) | (kase & 1)); 169 | } 170 | return r + delta; 171 | } 172 | if (r < crlo) { 173 | hi = m; 174 | } else { 175 | lo = m + 1; 176 | } 177 | } 178 | return r; 179 | } 180 | 181 | // to maps the rune to the specified case: UpperCase, LowerCase, or TitleCase. 182 | private static int to(int kase, int r) { 183 | return to(kase, r, UnicodeTables.CASE_RANGES); 184 | } 185 | 186 | // toUpper maps the rune to upper case. 187 | static int toUpper(int r) { 188 | if (r <= MAX_ASCII) { 189 | if ('a' <= r && r <= 'z') { 190 | r -= 'a' - 'A'; 191 | } 192 | return r; 193 | } 194 | return to(UnicodeTables.UpperCase, r); 195 | } 196 | 197 | // toLower maps the rune to lower case. 198 | static int toLower(int r) { 199 | if (r <= MAX_ASCII) { 200 | if ('A' <= r && r <= 'Z') { 201 | r += 'a' - 'A'; 202 | } 203 | return r; 204 | } 205 | return to(UnicodeTables.LowerCase, r); 206 | } 207 | 208 | // simpleFold iterates over Unicode code points equivalent under 209 | // the Unicode-defined simple case folding. Among the code points 210 | // equivalent to rune (including rune itself), SimpleFold returns the 211 | // smallest r >= rune if one exists, or else the smallest r >= 0. 212 | // 213 | // For example: 214 | // SimpleFold('A') = 'a' 215 | // SimpleFold('a') = 'A' 216 | // 217 | // SimpleFold('K') = 'k' 218 | // SimpleFold('k') = '\u212A' (Kelvin symbol, K) 219 | // SimpleFold('\u212A') = 'K' 220 | // 221 | // SimpleFold('1') = '1' 222 | // 223 | // Derived from Go's unicode.SimpleFold. 224 | // 225 | public static int simpleFold(int r) { 226 | // Consult caseOrbit table for special cases. 227 | int lo = 0; 228 | int hi = UnicodeTables.CASE_ORBIT.length; 229 | while (lo < hi) { 230 | int m = lo + (hi - lo) / 2; 231 | if (UnicodeTables.CASE_ORBIT[m][0] < r) { 232 | lo = m + 1; 233 | } else { 234 | hi = m; 235 | } 236 | } 237 | if (lo < UnicodeTables.CASE_ORBIT.length && 238 | UnicodeTables.CASE_ORBIT[lo][0] == r) { 239 | return UnicodeTables.CASE_ORBIT[lo][1]; 240 | } 241 | 242 | // No folding specified. This is a one- or two-element 243 | // equivalence class containing rune and toLower(rune) 244 | // and toUpper(rune) if they are different from rune. 245 | int l = toLower(r); 246 | if (l != r) { 247 | return l; 248 | } 249 | return toUpper(r); 250 | } 251 | 252 | private Unicode() {} // uninstantiable 253 | 254 | } 255 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/re2j/Utils.java: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package com.google.re2j; 6 | 7 | 8 | import com.livejournal.karino2.zipsourcecodereading.Slice; 9 | 10 | import static com.google.re2j.MachineInput.EOF; 11 | 12 | /** 13 | * Various constants and helper utilities. 14 | */ 15 | public abstract class Utils { 16 | 17 | static final int[] EMPTY_INTS = {}; 18 | 19 | // Returns true iff |c| is an ASCII letter or decimal digit. 20 | static boolean isalnum(int c) { 21 | return '0' <= c && c <= '9' || 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z'; 22 | } 23 | 24 | // If |c| is an ASCII hex digit, returns its value, otherwise -1. 25 | static int unhex(int c) { 26 | if ('0' <= c && c <= '9') { 27 | return c - '0'; 28 | } 29 | if ('a' <= c && c <= 'f') { 30 | return c - 'a' + 10; 31 | } 32 | if ('A' <= c && c <= 'F') { 33 | return c - 'A' + 10; 34 | } 35 | return -1; 36 | } 37 | 38 | private static final String METACHARACTERS = "\\.+*?()|[]{}^$"; 39 | 40 | // Appends a RE2 literal to |out| for rune |rune|, 41 | // with regexp metacharacters escaped. 42 | static void escapeRune(StringBuilder out, int rune) { 43 | if (Unicode.isPrint(rune)) { 44 | if (METACHARACTERS.indexOf((char) rune) >= 0) { 45 | out.append('\\'); 46 | } 47 | out.appendCodePoint(rune); 48 | return; 49 | } 50 | 51 | switch (rune) { 52 | case '"': out.append("\\\""); break; 53 | case '\\': out.append("\\\\"); break; 54 | case '\t': out.append("\\t"); break; 55 | case '\n': out.append("\\n"); break; 56 | case '\r': out.append("\\r"); break; 57 | case '\b': out.append("\\b"); break; 58 | case '\f': out.append("\\f"); break; 59 | default: { 60 | String s = Integer.toHexString(rune); 61 | if (rune < 0x100) { 62 | out.append("\\x"); 63 | if (s.length() == 1) { 64 | out.append('0'); 65 | } 66 | out.append(s); 67 | } else { 68 | out.append("\\x{").append(s).append('}'); 69 | } 70 | break; 71 | } 72 | } 73 | } 74 | 75 | // Returns the array of runes in the specified Java UTF-16 string. 76 | static int[] stringToRunes(String str) { 77 | int charlen = str.length(); 78 | int runelen = str.codePointCount(0, charlen); 79 | int[] runes = new int[runelen]; 80 | int r = 0, c = 0; 81 | while (c < charlen) { 82 | int rune = str.codePointAt(c); 83 | runes[r++] = rune; 84 | c += Character.charCount(rune); 85 | } 86 | return runes; 87 | } 88 | 89 | // Returns the Java UTF-16 string containing the single rune |r|. 90 | public static String runeToString(int r) { 91 | char c = (char) r; 92 | return r == c 93 | ? String.valueOf(c) 94 | : new String(Character.toChars(r)); 95 | // fix by karino : new String(Character.toChars(c)); 96 | } 97 | 98 | // Returns a new copy of the specified subarray. 99 | static int[] subarray(int[] array, int start, int end) { 100 | int[] r = new int[end - start]; 101 | for (int i = start; i < end; ++i) { 102 | r[i - start] = array[i]; 103 | } 104 | return r; 105 | } 106 | 107 | // Returns a new copy of the specified subarray. 108 | static byte[] subarray(byte[] array, int start, int end) { 109 | byte[] r = new byte[end - start]; 110 | for (int i = start; i < end; ++i) { 111 | r[i - start] = array[i]; 112 | } 113 | return r; 114 | } 115 | 116 | // Returns the index of the first occurrence of array |target| within 117 | // array |source| after |fromIndex|, or -1 if not found. 118 | static int indexOf(Slice source, Slice target, int fromIndex) { 119 | if (fromIndex >= source.length()) { 120 | return target.length() == 0 ? source.length() : -1; 121 | } 122 | if (fromIndex < 0) { 123 | fromIndex = 0; 124 | } 125 | if (target.length() == 0) { 126 | return fromIndex; 127 | } 128 | 129 | byte first = target.getByte(0); 130 | for (int i = fromIndex, max = source.length() - target.length(); i <= max; 131 | i++) { 132 | // Look for first byte. 133 | if (source.getByte(i) != first) { 134 | while (++i <= max && source.getByte(i) != first) {} 135 | } 136 | 137 | // Found first byte, now look at the rest of v2. 138 | if (i <= max) { 139 | int j = i + 1; 140 | int end = j + target.length() - 1; 141 | for (int k = 1; j < end && source.getByte(j) == target.getByte(k); j++, k++) {} 142 | 143 | if (j == end) { 144 | return i; // found whole array 145 | } 146 | } 147 | } 148 | return -1; 149 | } 150 | 151 | // isWordByte reports whether b is consider a ``word character'' 152 | // during the evaluation of the \b and \B zero-width assertions. 153 | // These assertions are ASCII-only: the word characters are [A-Za-z0-9_]. 154 | static boolean isWordByte(byte b) { 155 | return ('A' <= b && b <= 'Z' || 156 | 'a' <= b && b <= 'z' || 157 | '0' <= b && b <= '9' || 158 | b == '_'); 159 | } 160 | 161 | static boolean isRuneStart(byte b) { 162 | return (b & 0xC0) != 0x80; // 10xxxxxx 163 | } 164 | 165 | //// EMPTY_* flags 166 | 167 | static final int EMPTY_BEGIN_LINE = 0x01; 168 | static final int EMPTY_END_LINE = 0x02; 169 | static final int EMPTY_BEGIN_TEXT = 0x04; 170 | static final int EMPTY_END_TEXT = 0x08; 171 | static final int EMPTY_WORD_BOUNDARY = 0x10; 172 | static final int EMPTY_NO_WORD_BOUNDARY = 0x20; 173 | static final int EMPTY_ALL = -1; // (impossible) 174 | 175 | // emptyOpContext returns the zero-width assertions satisfied at the position 176 | // between the bytes b1 and b2, a bitmask of EMPTY_* flags. 177 | // Passing b1 == -1 indicates that the position is at the beginning of the text. 178 | // Passing b2 == -1 indicates that the position is at the end of the text. 179 | // TODO(adonovan): move to Machine. 180 | static int emptyOpContext(byte b1, byte b2) { 181 | int op = 0; 182 | if (b1 == EOF) { 183 | op |= EMPTY_BEGIN_TEXT | EMPTY_BEGIN_LINE; 184 | } 185 | if (b1 == '\n') { 186 | op |= EMPTY_BEGIN_LINE; 187 | } 188 | if (b2 == EOF) { 189 | op |= EMPTY_END_TEXT | EMPTY_END_LINE; 190 | } 191 | if (b2 == '\n') { 192 | op |= EMPTY_END_LINE; 193 | } 194 | if (isWordByte(b1) != isWordByte(b2)) { 195 | op |= EMPTY_WORD_BOUNDARY; 196 | } else { 197 | op |= EMPTY_NO_WORD_BOUNDARY; 198 | } 199 | return op; 200 | } 201 | 202 | static boolean arrayFirstElementsEqual(int[] a, int[] a2, int length) { 203 | if (a == a2) { 204 | return true; 205 | } 206 | 207 | if (a == null || a2 == null) { 208 | return false; 209 | } 210 | 211 | if (a.length < length || a2.length < length) { 212 | return false; 213 | } 214 | 215 | for (int i = 0; i < length; i++) 216 | if (a[i] != a2[i]) 217 | return false; 218 | 219 | return true; 220 | } 221 | 222 | private Utils() {} // uninstantiable 223 | 224 | } 225 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/re2j/make_perl_groups.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl 2 | # Copyright 2008 The Go Authors. All rights reserved. 3 | # Use of this source code is governed by a BSD-style 4 | # license that can be found in the LICENSE file. 5 | 6 | # Modified version of make_perl_groups.pl from RE2/Go: 7 | # code.google.com/p/go/source/browse/src/pkg/regexp/syntax/make_perl_groups.pl 8 | # which is in turn a modified version of RE2/C++ implementation. 9 | 10 | # Generate table entries giving character ranges 11 | # for POSIX/Perl character classes. Rather than 12 | # figure out what the definition is, it is easier to ask 13 | # Perl about each letter from 0-128 and write down 14 | # its answer. 15 | 16 | @posixclasses = ( 17 | "[:alnum:]", 18 | "[:alpha:]", 19 | "[:ascii:]", 20 | "[:blank:]", 21 | "[:cntrl:]", 22 | "[:digit:]", 23 | "[:graph:]", 24 | "[:lower:]", 25 | "[:print:]", 26 | "[:punct:]", 27 | "[:space:]", 28 | "[:upper:]", 29 | "[:word:]", 30 | "[:xdigit:]", 31 | ); 32 | 33 | @perlclasses = ( 34 | "\\d", 35 | "\\s", 36 | "\\w", 37 | ); 38 | 39 | sub ComputeClass($) { 40 | my @ranges; 41 | my ($class) = @_; 42 | my $regexp = "[$class]"; 43 | my $start = -1; 44 | for (my $i=0; $i<=129; $i++) { 45 | if ($i == 129) { $i = 256; } 46 | if ($i <= 128 && chr($i) =~ $regexp) { 47 | if ($start < 0) { 48 | $start = $i; 49 | } 50 | } else { 51 | if ($start >= 0) { 52 | push @ranges, [$start, $i-1]; 53 | } 54 | $start = -1; 55 | } 56 | } 57 | return @ranges; 58 | } 59 | 60 | sub PrintClass($$@) { 61 | my ($cname, $groupmap, $name, @ranges) = @_; 62 | print " private static final int[] code$cname = { /* $name */\n"; 63 | for (my $i=0; $i<@ranges; $i++) { 64 | my @a = @{$ranges[$i]}; 65 | printf "\t0x%x, 0x%x,\n", $a[0], $a[1]; 66 | } 67 | print " };\n\n"; 68 | my $n = @ranges; 69 | $negname = $name; 70 | if ($negname =~ /:/) { 71 | $negname =~ s/:/:^/; 72 | } else { 73 | $negname =~ y/a-z/A-Z/; 74 | } 75 | $name =~ s/\\/\\\\/g; 76 | $negname =~ s/\\/\\\\/g; 77 | return " $groupmap.put(\"$name\", \tnew CharGroup(+1, code$cname));\n" . 78 | " $groupmap.put(\"$negname\", \tnew CharGroup(-1, code$cname));\n"; 79 | } 80 | 81 | my $gen = 0; 82 | 83 | sub PrintClasses($@) { 84 | my ($cname, @classes) = @_; 85 | my $groupmap = uc($cname) . "_GROUPS"; 86 | my @entries; 87 | foreach my $cl (@classes) { 88 | my @ranges = ComputeClass($cl); 89 | push @entries, PrintClass(++$gen, $groupmap, $cl, @ranges); 90 | } 91 | print " static final HashMap $groupmap =\n"; 92 | print " new HashMap();\n"; 93 | print "\n"; 94 | print " static {\n"; 95 | foreach my $e (@entries) { 96 | print $e; 97 | } 98 | print " }\n"; 99 | my $count = @entries; 100 | } 101 | 102 | print <perl_groups.go 105 | 106 | package com.google.re2j; 107 | 108 | import java.util.HashMap; 109 | 110 | class CharGroup { 111 | 112 | final int sign; 113 | final int[] cls; 114 | 115 | private CharGroup(int sign, int[] cls) { 116 | this.sign = sign; 117 | this.cls = cls; 118 | } 119 | 120 | EOF 121 | 122 | PrintClasses("perl", @perlclasses); 123 | PrintClasses("posix", @posixclasses); 124 | 125 | 126 | print <UnicodeTables.java 18 | # 19 | # States: 20 | # 0 = toplevel 21 | # 1 = inside Scripts/Categories/Properties definition: 22 | # var Categories = map[string]*RangeTable{ 23 | # "Lm": Lm, 24 | # ... 25 | # } 26 | # 2 = inside a range definition: 27 | # var _Carian = &RangeTable{ 28 | # ... 29 | # R32: []Range32{ 30 | # {0x102a0, 0x102d0, 1}, 31 | # ... 32 | # }, 33 | # } 34 | # 3 = inside an alias definition: 35 | # var ( 36 | # Cc = _Cc; // comment 37 | # ... 38 | # ) 39 | # 4 = inside CaseRanges definition: 40 | # var _CaseRanges = []CaseRange{ 41 | # {0x0041, 0x005A, d{0, 32, 0}}, 42 | # ... 43 | # } 44 | # 5 = inside caseOrbit definition: 45 | # var caseOrbit = []foldPair{ 46 | # {0x004B, 0x006B}, 47 | # ... 48 | # } 49 | 50 | BEGIN { 51 | print "// AUTOGENERATED by make_unicode_tables.awk from the output of" 52 | print "// go/src/pkg/unicode/maketables.go. Yes it's awful, but frankly" 53 | print "// it's quicker than porting 1300 more lines of Go." 54 | print 55 | print "package com.google.re2j;"; 56 | print 57 | print "import java.util.HashMap;" 58 | print "import java.util.Map;" 59 | print 60 | print "class UnicodeTables {"; 61 | 62 | # Constants used by CASE_RANGES and by Unicode utilities. 63 | # TODO(adonovan): use Java-style identifiers. 64 | print " static final int UpperCase = 0;"; 65 | print " static final int LowerCase = 1;"; 66 | print " static final int TitleCase = 2;"; 67 | print " static final int UpperLower = 0x110000;"; 68 | } 69 | 70 | 71 | ### State 1 72 | 73 | state == 0 && /^var FoldScript = .*{}/ { 74 | # Special case for when this map is empty map 75 | print " private static Map " $2 "() {"; 76 | print " return new HashMap();"; 77 | print " }"; 78 | next; 79 | } 80 | state == 0 && /^var (Categories|Scripts|FoldCategory|FoldScript|Properties)/ { 81 | print " private static Map "$2"() {"; 82 | print " Map map = new HashMap();"; 83 | state = 1; 84 | next; 85 | } 86 | state == 1 && /.*: .*,/ { 87 | key = substr($1, 0, length($1) - 1); 88 | value = substr($2, 0, length($2) - 1); 89 | print " map.put(" key ", " value ");"; 90 | next; 91 | } 92 | state == 1 && /^}/ { 93 | print " return map;" 94 | print " }"; 95 | state = 0; 96 | next; 97 | } 98 | 99 | 100 | ### State 2 101 | 102 | state == 0 && /^var .* = &RangeTable{/ { 103 | # Hack upon hack: javac refuses to compile too-large methods, 104 | # so we have to split this into smaller pieces. 105 | print " private static final int[][] " $2 " = make" $2 "();"; 106 | print " private static int[][] make" $2 "() {"; 107 | print " return new int[][] {" 108 | state = 2; 109 | next; 110 | } 111 | state == 2 && / *R(16|32)/ { next; } 112 | state == 2 && /\t},/ { next; } 113 | state == 2 && /^}/ { 114 | print " };"; 115 | print " }"; 116 | state = 0; 117 | next; 118 | } 119 | state == 2 { print; } 120 | 121 | 122 | ### State 3 123 | 124 | state == 0 && /^var \(/ { 125 | state = 3; 126 | next; 127 | } 128 | state == 3 && /=/ { 129 | print " static final int[][] " $1 " = " $3 ";"; 130 | } 131 | state == 3 && /^)/ { 132 | state = 0; 133 | next; 134 | } 135 | 136 | ### State 4 137 | 138 | state == 0 && /^var _CaseRanges = / { 139 | print " static final int[][] CASE_RANGES = {"; 140 | state = 4; 141 | next; 142 | } 143 | state == 4 && /^}/ { 144 | state = 0; 145 | print " };" 146 | next; 147 | } 148 | state == 4 { 149 | sub("d{", ""); 150 | sub("}}", "}"); 151 | print; 152 | } 153 | 154 | ### State 5 155 | 156 | state == 0 && /^var caseOrbit = / { 157 | print " static final int[][] CASE_ORBIT = {"; 158 | state = 5; 159 | next; 160 | } 161 | state == 5 && /^}/ { 162 | state = 0; 163 | print " };" 164 | next; 165 | } 166 | state == 5 { 167 | print; 168 | } 169 | 170 | 171 | END { 172 | # Call the functions after all initialization has occurred. 173 | print " static final Map CATEGORIES = Categories();" 174 | print " static final Map SCRIPTS = Scripts();" 175 | print " static final Map PROPERTIES = Properties();" 176 | print " static final Map FOLD_CATEGORIES = FoldCategory();" 177 | print " static final Map FOLD_SCRIPT = FoldScript();" 178 | print "" 179 | print " private UnicodeTables() {} // uninstantiable"; 180 | print "}" 181 | } 182 | -------------------------------------------------------------------------------- /app/src/main/java/com/google/re2j/package.html: -------------------------------------------------------------------------------- 1 | 2 |

3 | This package provides an implementation of regular expression 4 | matching based on Russ Cox's linear-time RE2 algorithm. 6 |

7 |

8 | The API presented by {@code com.google.re2j} mimics that of {@code 9 | java.util.regex.Matcher} and {@code java.util.regex.Pattern}. 10 | While not identical, they are similar enough that most users can 11 | switch implementations simply by changing their {@code import}s. 12 |

13 |

14 | The syntax of the regular expressions accepted is the same general 15 | syntax used by Perl, Python, and other languages. More precisely, 16 | it is the syntax accepted by the C++ and Go implementations of RE2 17 | described at http://code.google.com/p/re2/wiki/Syntax, 19 | except for \C (match any byte), which is not 20 | supported because in this implementation, the matcher's input is 21 | conceptually a stream of Unicode code points, not bytes. 22 |

23 |

24 | The current API is rather small and intended for compatibility 25 | with {@code java.util.regex}, but the underlying implementation 26 | supports some additional features, such as the ability to process 27 | input character streams encoded as UTF-8 byte arrays. These may 28 | be exposed in a future release if there is sufficient interest. 29 |

30 |

31 | Example use: 32 |

33 |
34 |     import com.google.re2j.Matcher;
35 |     import com.google.re2j.Pattern;
36 | 
37 |     Pattern p = Pattern.compile("b(an)*(.)");
38 |     Matcher m = p.matcher("by, band, banana");
39 |     assertTrue(m.lookingAt());
40 | 
41 |     m.reset();                              // "by, band, banana"
42 |     assertTrue(m.find());
43 |     assertEquals("by", m.group(0));         //  --
44 |     assertEquals(null, m.group(1));         //
45 |     assertEquals("y",  m.group(2));         //   ^
46 |     assertTrue(m.find());
47 |     assertEquals("band", m.group(0));       //      ----
48 |     assertEquals("an",   m.group(1));       //       ^^
49 |     assertEquals("d",    m.group(2));       //         ^
50 |     assertTrue(m.find());
51 |     assertEquals("banana", m.group(0));     //            -------
52 |     assertEquals("an",     m.group(1));     //             ^^
53 |     assertEquals("a",      m.group(2));     //                  ^
54 |     assertFalse(m.find());
55 |   
56 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/IndexingService.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading 2 | 3 | import android.app.IntentService 4 | import android.app.NotificationManager 5 | import android.app.PendingIntent 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.os.Handler 9 | import android.support.v4.app.NotificationCompat 10 | import com.livejournal.karino2.zipsourcecodereading.index.IndexException 11 | import com.livejournal.karino2.zipsourcecodereading.index.IndexWriter 12 | import java.io.DataInputStream 13 | import java.io.File 14 | import java.util.zip.ZipFile 15 | 16 | /** 17 | * Created by _ on 2017/09/21. 18 | */ 19 | class IndexingService : IntentService("IndexingService") { 20 | companion object { 21 | const val NOTIFICATION_ID = 1 22 | } 23 | 24 | val handler = Handler() 25 | 26 | fun showMessage(msg : String) = handler.post { MainActivity.showMessage(this, msg) } 27 | 28 | override fun onHandleIntent(intent: Intent?) { 29 | intent?.let { 30 | val zipFile = File(intent.getStringExtra("ZIP_PATH")) 31 | val indexCandidate = ZipChooseActivity.indexCandidate(zipFile) 32 | val tmpFile = ZipChooseActivity.getTempDirectory() 33 | 34 | val indexWriter = IndexWriter(tmpFile, indexCandidate) 35 | 36 | val zipArchive = SourceArchive(ZipFile(zipFile)) 37 | var count = 0 38 | zipArchive.listFiles() 39 | .filter{ !File(it.name).name.startsWith(".") } 40 | .doOnNext { handler.post { updateNotification(count++) }} 41 | .map { try { indexWriter.add(it.toString(), DataInputStream(zipArchive.getInputStream(it))); 1 } catch(e: IndexException) { 0 } } 42 | .subscribe() 43 | showMessageNotification("start flush") 44 | indexWriter.flush() 45 | showMessageNotification("end flush") 46 | showMessageNotification("start notification") 47 | handler.post { showReadyNotification(zipFile) } 48 | return 49 | } 50 | showMessage("recreate case, NYI.") 51 | } 52 | 53 | val notificationManager : NotificationManager by lazy { getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } 54 | 55 | val notificationBuilder: android.support.v4.app.NotificationCompat.Builder by lazy { 56 | NotificationCompat.Builder(this) 57 | .setSmallIcon(R.mipmap.ic_launcher) 58 | } 59 | 60 | fun showMessageNotification(msg : String) = handler.post { _showMessageNotification(msg) } 61 | fun _showMessageNotification(msg : String) { 62 | notificationBuilder 63 | .setContentTitle("Indexing status") 64 | .setContentText(msg) 65 | 66 | notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()) 67 | 68 | } 69 | 70 | fun updateNotification(fileCount: Int) { 71 | notificationBuilder 72 | .setContentTitle("Indexing...") 73 | .setContentText("Indexing $fileCount file.") 74 | 75 | notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()) 76 | 77 | } 78 | 79 | private fun showReadyNotification(zipFile: File) { 80 | notificationBuilder 81 | .setContentTitle("Index ready") 82 | .setContentText("Index ready") 83 | 84 | val intent = Intent(this, SearchActivity::class.java) 85 | 86 | val pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) 87 | notificationBuilder.setContentIntent(pendingIntent) 88 | 89 | notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build()) 90 | 91 | 92 | // This bring activity without user intention. 93 | // I think this is not good, but implement proper way is a little complex. 94 | // Also, currently we do not support process recycle during indexing. 95 | // So I assume ZipChooseActivity is active at this time, and this assumption is true for most of time. 96 | val intent2 = Intent(this, ZipChooseActivity::class.java) 97 | intent2.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP 98 | startActivity(intent2) 99 | } 100 | 101 | } -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.support.v7.app.AppCompatActivity 6 | import android.os.Bundle 7 | import android.widget.Toast 8 | import java.io.File 9 | 10 | class MainActivity : AppCompatActivity() { 11 | companion object { 12 | const val LAST_ZIP_PATH_KEY = "last_zip_path" 13 | fun lastZipPath(ctx: Context) = sharedPreferences(ctx).getString(LAST_ZIP_PATH_KEY, null) 14 | fun writeLastZipPath(ctx: Context, path : String) = sharedPreferences(ctx).edit() 15 | .putString(LAST_ZIP_PATH_KEY, path) 16 | .commit() 17 | 18 | private fun sharedPreferences(ctx: Context) = ctx.getSharedPreferences("ZSCR_PREFS", Context.MODE_PRIVATE) 19 | 20 | fun showMessage(ctx: Context, msg : String) = Toast.makeText(ctx, msg, Toast.LENGTH_SHORT).show() 21 | 22 | } 23 | 24 | fun requestedZipPath() : String? { 25 | getIntent()?.data?.path?.let { 26 | writeLastZipPath(this, it) 27 | return it 28 | } 29 | return lastZipPath(this) 30 | } 31 | 32 | override fun onCreate(savedInstanceState: Bundle?) { 33 | super.onCreate(savedInstanceState) 34 | 35 | val zipPath = requestedZipPath() 36 | zipPath?.let { 37 | val idx = ZipChooseActivity.findIndex(File(zipPath)) 38 | idx?.let { 39 | val intent = Intent(this, SearchActivity::class.java) 40 | startActivity(intent) 41 | finish() 42 | return 43 | } 44 | } 45 | 46 | val intent = Intent(this, ZipChooseActivity::class.java) 47 | startActivity(intent) 48 | 49 | finish(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/ObserverAdapter.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading 2 | 3 | import android.support.v7.widget.RecyclerView 4 | import io.reactivex.Observable 5 | import io.reactivex.subjects.PublishSubject 6 | 7 | /** 8 | * Created by _ on 2017/08/29. 9 | */ 10 | abstract class ObserverAdapter : RecyclerView.Adapter() { 11 | override fun getItemCount(): Int { 12 | return items.size 13 | } 14 | 15 | val items = ArrayList() 16 | 17 | fun addAll(more: List) { 18 | items.addAll(more) 19 | subject.onNext(this) 20 | } 21 | 22 | val subject : PublishSubject> = PublishSubject.create() 23 | 24 | fun datasetChangedNotifier() : Observable> { 25 | return subject 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/QueryOp.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading 2 | 3 | enum class QueryOp { 4 | ALL, // Everything matches 5 | NONE, // Nothing matches 6 | AND, // All in Sub and Trigram must match 7 | OR // At least one in Sub or Trigram must match 8 | } -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/RegexpReader.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading 2 | 3 | import com.google.re2j.Pattern 4 | import io.reactivex.Observable 5 | import java.io.InputStream 6 | 7 | /** 8 | * Created by _ on 2017/08/22. 9 | */ 10 | class RegexpReader(val pat : Pattern) { 11 | val buf : ByteArray by lazy { 12 | ByteArray(1024*1024) 13 | } 14 | 15 | val nl = '\n'.toByte() 16 | 17 | fun findEol(buf: ByteArray, startpos: Int, upperbound: Int, endText : Boolean) : Int? { 18 | for( i in startpos until upperbound) { 19 | if(buf[i] == nl) 20 | return i 21 | } 22 | if(endText) 23 | return upperbound 24 | return null 25 | } 26 | 27 | fun findLastIndexOfRange(buf: ByteArray, elem : Byte, beg : Int, end : Int) : Int { 28 | for (i in (end-1) downTo beg) { 29 | if(buf[i] == elem) 30 | return i 31 | } 32 | return -1 33 | } 34 | 35 | data class MatchEntry(val fentry: String, val lineNumber: Int?, val line: String) 36 | 37 | // port of codesearch/regexp/match.go Read() 38 | fun Read(inp: InputStream, fentry : String, lineno : Int? = null) : Observable = Observable.create { emitter -> 39 | 40 | var bufUsed = 0 41 | var endText = false 42 | var end = 0 43 | var lineNumber: Int? = lineno 44 | 45 | while (!emitter.isDisposed) { 46 | 47 | val readLen = inp.read(buf, bufUsed, buf.size - bufUsed) 48 | end = bufUsed 49 | 50 | if (readLen == -1) { 51 | endText = true 52 | } else { 53 | bufUsed += readLen 54 | end = findLastIndexOfRange(buf, nl, 0, bufUsed) + 1 55 | } 56 | 57 | var chunkStart = 0 58 | val slice = Slice.wrappedBuffer(buf, chunkStart, end-chunkStart) 59 | 60 | while (chunkStart < end) { 61 | slice.set(buf, chunkStart, end-chunkStart) 62 | val matcher = pat.matcher(slice) 63 | 64 | if (!matcher.find()) 65 | break 66 | 67 | val matchpos = matcher.start() + chunkStart 68 | val lineEnd = findEol(buf, matchpos, end, endText) ?: break 69 | 70 | var lineStart = findLastIndexOfRange(buf, nl, chunkStart, matchpos)+1 71 | if(lineStart == 0) { 72 | lineStart = chunkStart 73 | } 74 | 75 | 76 | lineNumber?.let { lineNumber += countNL(buf, chunkStart, lineStart) } 77 | 78 | val line = buf.copyOfRange(lineStart, lineEnd).toString(Charsets.UTF_8) 79 | 80 | emitter.onNext(MatchEntry(fentry, lineNumber, line)) 81 | lineNumber?.let { lineNumber++ } 82 | 83 | chunkStart = lineEnd+1 84 | } 85 | 86 | lineNumber?.let { lineNumber += countNL(buf, chunkStart, end) } 87 | 88 | 89 | System.arraycopy(buf, end, buf, 0, bufUsed - end) 90 | bufUsed -= end 91 | 92 | if(bufUsed ==0 && endText) { 93 | break 94 | } 95 | } 96 | emitter.onComplete() 97 | } 98 | 99 | private fun countNL(buf: ByteArray, start: Int, end: Int): Int { 100 | return buf.slice(start until end).count { it == nl } 101 | } 102 | 103 | } -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/RxPrettifyParser.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading 2 | 3 | /** 4 | * Created by _ on 2017/10/03. 5 | */ 6 | 7 | import io.reactivex.Observable 8 | import java.util.Arrays 9 | import prettify.parser.Job 10 | import prettify.parser.Prettify 11 | import syntaxhighlight.ParseResult 12 | 13 | /** 14 | * The cancelable prettify parser for syntax highlight. 15 | */ 16 | class RxPrettifyParser { 17 | /** 18 | * The prettify parser. 19 | */ 20 | val prettify = Prettify() 21 | 22 | fun parse(fileExtension: String, content: String): Observable { 23 | return Observable.create { 24 | emitter -> 25 | 26 | val job = Job(0, content) 27 | prettify.langHandlerForExtension(fileExtension, content).decorate(job) 28 | val decorations = job.decorations 29 | var i = 0 30 | val iEnd = decorations.size 31 | while (i < iEnd) { 32 | if(emitter.isDisposed) { 33 | return@create 34 | } 35 | val endPos = if (i + 2 < iEnd) decorations[i + 2] as Int else content.length 36 | val startPos = decorations[i] as Int 37 | emitter.onNext(ParseResult(startPos, endPos - startPos, Arrays.asList(*arrayOf(decorations[i + 1] as String)))) 38 | i += 2 39 | } 40 | emitter.onComplete() 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/SearchActivity.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.support.v7.app.AppCompatActivity 6 | import android.os.Bundle 7 | import android.os.Handler 8 | import android.support.v7.widget.DividerItemDecoration 9 | import android.support.v7.widget.LinearLayoutManager 10 | import android.support.v7.widget.RecyclerView 11 | import android.support.v7.widget.Toolbar 12 | import android.view.* 13 | import android.view.inputmethod.EditorInfo 14 | import android.view.inputmethod.InputMethodManager 15 | import android.widget.EditText 16 | import android.widget.TextView 17 | import com.google.re2j.Parser 18 | import com.google.re2j.Pattern 19 | import com.google.re2j.PatternSyntaxException 20 | import com.google.re2j.RE2 21 | import io.reactivex.Observable 22 | import io.reactivex.android.schedulers.AndroidSchedulers 23 | import io.reactivex.disposables.Disposable 24 | import io.reactivex.schedulers.Schedulers 25 | import com.livejournal.karino2.zipsourcecodereading.index.Index 26 | import io.reactivex.subjects.PublishSubject 27 | import java.io.File 28 | import java.util.concurrent.TimeUnit 29 | import java.util.zip.ZipEntry 30 | import java.util.zip.ZipFile 31 | 32 | class SearchActivity : AppCompatActivity() { 33 | 34 | val sourceArchive : SourceArchive by lazy { 35 | SourceArchive(ZipFile(MainActivity.lastZipPath(this))) 36 | } 37 | 38 | val index : Index by lazy { 39 | Index.open(ZipChooseActivity.findIndex(File(MainActivity.lastZipPath(this)))!!) 40 | } 41 | 42 | class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) { 43 | val lineNumberTV = (view.findViewById(R.id.lineNumber) as TextView) 44 | val filePathTV = (view.findViewById(R.id.filePath) as TextView) 45 | val matchLineTV = (view.findViewById(R.id.matchLine) as TextView) 46 | } 47 | 48 | // fun showMessage(msg : String) = MainActivity.showMessage(this, msg) 49 | 50 | inner class MatchEntryAdapter : ObserverAdapter() { 51 | override fun onBindViewHolder(holder: ViewHolder, position: Int) { 52 | val item = items[position] 53 | holder.lineNumberTV.text = (item.lineNumber!!).toString() 54 | // holder.lineNumberTV.text = "___" 55 | holder.filePathTV.text = item.fentry 56 | holder.matchLineTV.text = item.line 57 | 58 | with(holder.view) { 59 | tag = item 60 | setOnClickListener { openFile(holder.filePathTV.text.toString(), holder.lineNumberTV.text.toString().toInt()) } 61 | } 62 | } 63 | 64 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { 65 | val inflater = LayoutInflater.from(parent.context) 66 | return ViewHolder(inflater.inflate(R.layout.search_result_item, parent, false)) 67 | } 68 | 69 | } 70 | 71 | val searchAdapter = MatchEntryAdapter() 72 | 73 | var prevSearch : Disposable? = null 74 | 75 | fun showSearchingIndicator() { 76 | (findViewById(R.id.progressBar)).visibility = View.VISIBLE 77 | } 78 | 79 | fun hideSearchingIndicator() { 80 | (findViewById(R.id.progressBar)).visibility = View.GONE 81 | 82 | } 83 | 84 | val searchEntryField by lazy { 85 | findViewById(R.id.searchEntryField) 86 | } 87 | 88 | fun startSearch() { 89 | prevSearch?.dispose() 90 | prevSearch = null 91 | 92 | 93 | searchAdapter.items.clear() 94 | searchAdapter.notifyDataSetChanged() 95 | 96 | 97 | val fpat = findViewById(R.id.fileEntryField).text.toString() 98 | val spat = searchEntryField.text.toString() 99 | 100 | val ffilter = fun(path : String) : Boolean { 101 | if(fpat == "") 102 | return true 103 | return path.contains(fpat) 104 | } 105 | 106 | 107 | try { 108 | val reader = RegexpReader(Pattern.compile(spat)) 109 | 110 | // we should get regexp from pattern, but 111 | val query = Query.fromRegexp(Parser.parse(spat, RE2.PERL)) 112 | 113 | val obs = Observable.defer { Observable.fromIterable(index.postingQuery(query)) } 114 | .map { index.readName(it) } 115 | .filter { ffilter(it) } 116 | .flatMap { reader.Read(sourceArchive.getInputStream(ZipEntry(it)), it, 0) } 117 | .subscribeOn(Schedulers.io()) 118 | .buffer(1, TimeUnit.SECONDS, 5) 119 | 120 | showSearchingIndicator() 121 | 122 | prevSearch = obs.observeOn(AndroidSchedulers.mainThread()) 123 | .doOnComplete { 124 | prevSearch = null 125 | hideSearchingIndicator() 126 | } 127 | .subscribe { matches -> 128 | if (matches.size > 0) 129 | searchAdapter.addAll(matches) 130 | } 131 | }catch(e: PatternSyntaxException) { 132 | showMessage("Rexp compile fail: ${e.toString()}") 133 | } 134 | } 135 | 136 | 137 | override fun onCreate(savedInstanceState: Bundle?) { 138 | super.onCreate(savedInstanceState) 139 | setContentView(R.layout.activity_search) 140 | 141 | val toolbar = (findViewById(R.id.toolbar) as Toolbar) 142 | setSupportActionBar(toolbar) 143 | 144 | supportActionBar!!.title = sourceArchive.title 145 | 146 | assert(MainActivity.lastZipPath(this) != null) 147 | 148 | val recycle = (findViewById(R.id.searchResult) as RecyclerView) 149 | recycle.adapter = searchAdapter 150 | searchAdapter.datasetChangedNotifier() 151 | .observeOn(AndroidSchedulers.mainThread()) 152 | .subscribe { ada -> 153 | // ada.notifyItemRangeInserted(0, ada.items.size) 154 | ada.notifyDataSetChanged() 155 | } 156 | 157 | val divider = DividerItemDecoration(this, (recycle.layoutManager as LinearLayoutManager).orientation) 158 | recycle.addItemDecoration(divider) 159 | 160 | 161 | var searchPublisher = PublishSubject.create() 162 | searchPublisher.throttleFirst(500L, TimeUnit.MILLISECONDS) 163 | .subscribe() { 164 | when(it) { 165 | EditorInfo.IME_ACTION_SEARCH-> { 166 | hideSoftkey() 167 | startSearch() 168 | } 169 | EditorInfo.IME_ACTION_UNSPECIFIED -> { 170 | startSearch(); 171 | } 172 | } 173 | } 174 | 175 | 176 | (findViewById(R.id.searchEntryField) as EditText).setOnEditorActionListener(fun(_, actionId, _) : Boolean { 177 | when(actionId) { 178 | EditorInfo.IME_ACTION_SEARCH, EditorInfo.IME_ACTION_UNSPECIFIED /* for hardware keyboard. */ -> { 179 | searchPublisher.onNext(actionId) 180 | return true 181 | } 182 | 183 | } 184 | return false; 185 | }) 186 | 187 | intent?.let { 188 | val word = intent.getStringExtra("SEARCH_WORD") 189 | if(!word.isNullOrEmpty()) { 190 | searchEntryField.setText(word) 191 | handler.post { startSearch() } 192 | } 193 | } 194 | 195 | } 196 | 197 | val handler by lazy { Handler() } 198 | 199 | fun hideSoftkey() { 200 | val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 201 | imm.hideSoftInputFromWindow(currentFocus.windowToken, 0) 202 | } 203 | 204 | private fun openFile(ent: String, lineNum: Int) { 205 | val intent = Intent(this, SourceViewActivity::class.java) 206 | intent.putExtra("ZIP_FILE_ENTRY", ent) 207 | intent.putExtra("LINE_NUM", lineNum) 208 | startActivity(intent) 209 | } 210 | 211 | fun showMessage(msg : String) = MainActivity.showMessage(this, msg) 212 | 213 | override fun onCreateOptionsMenu(menu: Menu?): Boolean { 214 | menuInflater.inflate(R.menu.search, menu) 215 | return true 216 | } 217 | 218 | override fun onOptionsItemSelected(item: MenuItem?): Boolean { 219 | when(item?.itemId) { 220 | R.id.action_choose -> { 221 | val intent = Intent(this, ZipChooseActivity::class.java) 222 | startActivity(intent) 223 | return true 224 | } 225 | R.id.action_filer -> { 226 | val intent = Intent(this, ZipFilerActivity::class.java) 227 | startActivity(intent) 228 | return true 229 | } 230 | } 231 | return super.onOptionsItemSelected(item) 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/Slice.java: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading; 2 | 3 | /** 4 | * Created by _ on 2017/08/28. 5 | */ 6 | 7 | public class Slice { 8 | byte[] buf; 9 | int offset; 10 | int length; 11 | public Slice(byte[] buf, int offset, int len) { 12 | this.buf = buf; 13 | this.offset = offset; 14 | this.length = len; 15 | } 16 | 17 | public void set(byte[] buf, int offset, int len) { 18 | this.buf = buf; 19 | this.offset = offset; 20 | this.length = len; 21 | } 22 | 23 | static Slice _EMPTY_SLICE = null; 24 | public static Slice getEmptySlice() { 25 | if(_EMPTY_SLICE == null){ 26 | _EMPTY_SLICE = new Slice(new byte[]{}, 0, 0); 27 | } 28 | return _EMPTY_SLICE; 29 | } 30 | public static Slice wrappedBuffer(byte[] buf, int offset, int len) { 31 | return new Slice(buf, offset, len); 32 | } 33 | public static Slice wrappedBuffer(byte[] buf) { 34 | return new Slice(buf, 0, buf.length); 35 | } 36 | 37 | public int length() { return length; } 38 | public byte getByte(int pos) { 39 | return buf[offset+pos]; 40 | } 41 | 42 | public byte[] getBuf() { return buf; } 43 | 44 | public Slice slice(int offset, int len) { 45 | return new Slice(buf, offset, len); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/SourceArchive.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading 2 | 3 | import io.reactivex.Observable 4 | import java.io.File 5 | import java.io.InputStream 6 | import java.util.zip.ZipEntry 7 | import java.util.zip.ZipFile 8 | 9 | /** 10 | * Created by _ on 2017/08/19. 11 | */ 12 | class SourceArchive(val zipFile : ZipFile) { 13 | 14 | fun listFiles() : Observable { 15 | return Observable.create { 16 | emitter -> 17 | 18 | val entries = zipFile.entries() 19 | while(entries.hasMoreElements()) { 20 | if(emitter.isDisposed) 21 | return@create 22 | val next = entries.nextElement() 23 | emitter.onNext(next) 24 | } 25 | emitter.onComplete() 26 | } 27 | } 28 | 29 | fun ZipEntry.isUnder(dir: ZipEntryAux) : Boolean { 30 | return this.name.startsWith(dir.name) and (this.name.length != dir.nameWithSlash.length) 31 | } 32 | 33 | fun ZipEntry.isDirectlyUnder(dir: ZipEntryAux): Boolean { 34 | if(!this.isUnder(dir)) 35 | return false 36 | 37 | if(this.name.slice(dir.nameWithSlash.length until this.name.length).contains("/")) 38 | return false 39 | return true 40 | } 41 | 42 | /* 43 | fun listFilesAtRoot() : List { 44 | val res = ArrayList() 45 | val entries = zipFile.entries() 46 | 47 | while(entries.hasMoreElements()) { 48 | val next = entries.nextElement() 49 | 50 | val name = next.name 51 | if(!name.slice(1 until name.length).contains("/")){ 52 | res.add(ZipEntryAux(next)) 53 | } 54 | } 55 | return res 56 | } 57 | */ 58 | 59 | fun ZipEntry.relative(parent: ZipEntryAux) = this.name.slice(parent.nameWithSlash.length until name.length) 60 | 61 | val File.topDirectory 62 | get() : File { 63 | var res = this 64 | while(res.parentFile != null){ 65 | res = res.parentFile 66 | } 67 | return res 68 | } 69 | 70 | fun listFilesAtDir(dir: ZipEntryAux) : List { 71 | val files = ArrayList() 72 | val entries = zipFile.entries() 73 | 74 | 75 | 76 | val dirs = mutableSetOf() 77 | 78 | while(entries.hasMoreElements()) { 79 | val next = entries.nextElement() 80 | if (next.isUnder(dir)) { 81 | if(next.isDirectlyUnder(dir)) { 82 | files.add(ZipEntryAux(next)) 83 | } else { 84 | if(dir.isRoot) { 85 | dirs.add(File(next.name).topDirectory.name) 86 | 87 | } else { 88 | dirs.add(File(dir.nameWithSlash, File(next.relative(dir)).topDirectory.name).path) 89 | } 90 | } 91 | } 92 | } 93 | return dirs.sorted().map { ZipEntryAux(it) } + files 94 | 95 | } 96 | fun listFilesAt(dir: ZipEntryAux?) : List { 97 | return if(dir == null) listFilesAtDir(ZipEntryAux("")) else listFilesAtDir(dir) 98 | } 99 | 100 | fun getInputStream(ent: ZipEntry): InputStream { 101 | return zipFile.getInputStream(ent) 102 | } 103 | 104 | val title: String 105 | get() = zipFile.name 106 | 107 | } -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/ZipChooseActivity.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.content.pm.PackageManager 7 | import android.net.Uri 8 | import android.support.v7.app.AppCompatActivity 9 | import android.os.Bundle 10 | import android.os.Environment 11 | import android.os.Handler 12 | import android.provider.DocumentsContract 13 | import android.support.v4.app.ActivityCompat 14 | import android.support.v4.content.ContextCompat 15 | import android.view.View 16 | import android.widget.EditText 17 | import android.widget.Toast 18 | import java.io.File 19 | import java.io.IOException 20 | 21 | 22 | class ZipChooseActivity : AppCompatActivity() { 23 | 24 | companion object { 25 | const val REQUEST_PICK_ZIP = 1 26 | const val FOLDER_NAME = "ZipSourceCodeReading" 27 | 28 | @Throws(IOException::class) 29 | fun ensureDirExist(dir: File) { 30 | if (!dir.exists()) { 31 | if (!dir.mkdir()) { 32 | throw IOException() 33 | } 34 | } 35 | } 36 | 37 | @Throws(IOException::class) 38 | fun getStoreDirectory(): File { 39 | val dir = File(Environment.getExternalStorageDirectory(), FOLDER_NAME) 40 | ensureDirExist(dir) 41 | return dir 42 | } 43 | 44 | @Throws(IOException::class) 45 | fun getTempDirectory(): File { 46 | val dir = File(getStoreDirectory(), "tmp") 47 | ensureDirExist(dir) 48 | return dir 49 | } 50 | 51 | fun findIndex(zipPath: File) : File? { 52 | val cand1 = indexCandidate(zipPath) 53 | if(cand1.exists()) 54 | return cand1 55 | val cand2 = File(zipPath.absolutePath + ".idx") 56 | if(cand2.exists()) 57 | return cand2 58 | return null 59 | } 60 | 61 | fun indexCandidate(zipPath: File): File { 62 | return File(getStoreDirectory(), zipPath.name + ".idx") 63 | } 64 | 65 | } 66 | 67 | override fun onNewIntent(intent: Intent?) { 68 | // indexing finished. goto search activity. 69 | replaceToSearchActivity() 70 | } 71 | 72 | private val zipPathField: EditText 73 | get() { 74 | val et = findViewById(R.id.zipPathField) as EditText 75 | return et 76 | } 77 | 78 | val PERMISSION_REQUEST_READ_AND_WRITE_EXTERNAL_STORAGE_ID = 1 79 | 80 | override fun onCreate(savedInstanceState: Bundle?) { 81 | super.onCreate(savedInstanceState) 82 | setContentView(R.layout.activity_zip_choose) 83 | 84 | val zipPath = MainActivity.lastZipPath(this) 85 | zipPath?.let{ zipPathField.setText(zipPath) } 86 | 87 | findViewById(R.id.browseZipButton).setOnClickListener { _ -> 88 | val intent = Intent(Intent.ACTION_GET_CONTENT) 89 | intent.setType("application/zip") 90 | startActivityForResult(intent, REQUEST_PICK_ZIP); 91 | } 92 | 93 | findViewById(R.id.indexStartButton).setOnClickListener { _ -> 94 | val path = zipPathField.text.toString() 95 | onZipPathChosen(path) 96 | } 97 | 98 | requestReadAndWriteExternalStorage() 99 | } 100 | 101 | private fun onZipPathChosen(path: String) { 102 | MainActivity.writeLastZipPath(this, path) 103 | 104 | // val zipIS = ZipInputStream(contentResolver.openInputStream(Uri.parse(path))) 105 | 106 | val zipFile = File(path) 107 | val indexFile = findIndex(zipFile) 108 | indexFile?.let { 109 | replaceToSearchActivity() 110 | return 111 | } 112 | startIndexingService(zipFile) 113 | 114 | } 115 | 116 | private fun replaceToSearchActivity() { 117 | val intent = Intent(this, SearchActivity::class.java) 118 | startActivity(intent) 119 | finish() 120 | } 121 | 122 | private fun startIndexingService(zipFile: File) { 123 | findViewById(R.id.indexStartButton).isEnabled = false 124 | showMessage("Start indexing...") 125 | 126 | val intent = Intent(this, IndexingService::class.java) 127 | 128 | intent.putExtra("ZIP_PATH", zipFile.absolutePath) 129 | startService(intent) 130 | } 131 | 132 | fun showMessage(msg : String) = MainActivity.showMessage(this, msg) 133 | 134 | fun externalStorageDir(storageType : String) : File { 135 | val primary = Environment.getExternalStorageDirectory() 136 | if(storageType == "primary") { 137 | return primary 138 | } 139 | val extdirs = getExternalFilesDirs(null) 140 | val suffix = extdirs[0].absolutePath.substring(primary.absolutePath.length) 141 | for(file in getExternalFilesDirs(null)) { 142 | val abspath = file.absolutePath 143 | val dirstr = abspath.substring(0 until (abspath.length - suffix.length)) 144 | val dir = File(dirstr) 145 | if(dir.name.equals(storageType)) 146 | return dir; 147 | 148 | } 149 | 150 | throw IllegalArgumentException("No storage found.") 151 | 152 | } 153 | 154 | fun Uri.toPath() : String { 155 | // val sel = "${MediaStore.Files.FileColumns._ID}=?" 156 | 157 | if("com.android.externalstorage.documents".equals(this.authority)) { 158 | val docId = DocumentsContract.getDocumentId(this) 159 | val split = docId.split(":") 160 | 161 | return externalStorageDir(split[0]).absolutePath + "/${split[1]}" 162 | } 163 | return this.path 164 | 165 | } 166 | 167 | override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { 168 | when(requestCode) { 169 | REQUEST_PICK_ZIP ->{ 170 | if(resultCode == RESULT_OK) { 171 | data?.getData()?.let { zipPathField.setText(it.toPath()) } 172 | } 173 | return 174 | } 175 | } 176 | 177 | super.onActivityResult(requestCode, resultCode, data) 178 | } 179 | 180 | 181 | fun requestReadAndWriteExternalStorage(){ 182 | 183 | val isAllowedPermissionReadExternalStorage = ContextCompat.checkSelfPermission( 184 | this, Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED 185 | 186 | val isAllowedPermissionWriteExternalStorage = ContextCompat.checkSelfPermission( 187 | this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED 188 | 189 | if (isAllowedPermissionReadExternalStorage && isAllowedPermissionWriteExternalStorage) { 190 | return 191 | } 192 | 193 | ActivityCompat.requestPermissions(this, 194 | arrayOf(android.Manifest.permission.READ_EXTERNAL_STORAGE, 195 | android.Manifest.permission.WRITE_EXTERNAL_STORAGE), 196 | PERMISSION_REQUEST_READ_AND_WRITE_EXTERNAL_STORAGE_ID) 197 | 198 | } 199 | 200 | val handler by lazy { Handler() } 201 | 202 | override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { 203 | when (requestCode) { 204 | PERMISSION_REQUEST_READ_AND_WRITE_EXTERNAL_STORAGE_ID ->{ 205 | if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { 206 | return 207 | } 208 | } 209 | } 210 | 211 | Toast.makeText(this, "Not enough permission. Close app.", Toast.LENGTH_LONG ).show(); 212 | handler.postDelayed({finish()}, 200) 213 | } 214 | } 215 | 216 | 217 | -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/ZipEntryAux.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading 2 | 3 | import java.io.File 4 | import java.util.zip.ZipEntry 5 | 6 | /** 7 | * Created by _ on 2017/09/17. 8 | */ 9 | data class ZipEntryAux(val original : ZipEntry?, val fullPath: String) { 10 | 11 | companion object { 12 | fun ZipEntry.toDisplayName() : String { 13 | val suffix = if(this.isDirectory) "/" else "" 14 | return File(this.name).name + suffix 15 | } 16 | } 17 | 18 | val isRoot = (original == null) and (fullPath == "") 19 | 20 | constructor(org: ZipEntry) : this(org, org.name) 21 | constructor(fullPath: String): this(null, fullPath) 22 | 23 | val isDirectory = if(original == null) true else original.isDirectory 24 | val name = if(original == null) fullPath else original.name 25 | 26 | val nameWithSlash = if(name.endsWith("/")) name else name + "/" 27 | 28 | val _displayName = File(name).name 29 | val displayName = if(!isDirectory or _displayName.endsWith("/")) _displayName else _displayName + "/" 30 | 31 | val parent : ZipEntryAux 32 | get() = ZipEntryAux(File(fullPath).parentFile?.path ?: "") 33 | 34 | } -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/ZipFilerActivity.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading 2 | 3 | import android.content.Intent 4 | import android.support.v7.app.AppCompatActivity 5 | import android.os.Bundle 6 | import android.view.MenuItem 7 | import android.widget.AdapterView 8 | import android.widget.ArrayAdapter 9 | import android.widget.ListView 10 | import java.util.zip.ZipEntry 11 | import java.util.zip.ZipFile 12 | 13 | class ZipFilerActivity : AppCompatActivity() { 14 | 15 | val lastZipPath by lazy { 16 | MainActivity.lastZipPath(this) 17 | } 18 | 19 | val zipArchive by lazy { SourceArchive(ZipFile(lastZipPath)) } 20 | 21 | val listView by lazy { findViewById(R.id.filerListView) as ListView } 22 | 23 | var currentFolder : ZipEntryAux? = null 24 | var entries : List = arrayListOf() 25 | 26 | override fun onCreate(savedInstanceState: Bundle?) { 27 | super.onCreate(savedInstanceState) 28 | setContentView(R.layout.activity_zip_filer) 29 | 30 | supportActionBar?.let { 31 | it.setDisplayHomeAsUpEnabled(true) 32 | } 33 | 34 | listView.onItemClickListener = AdapterView.OnItemClickListener { _, _, i, _ -> 35 | val item = entries[i] 36 | if(item.isDirectory) { 37 | showContent(item) 38 | } else { 39 | // dummy entry is always directory. so in this section, item always has original. 40 | openFile(item.original!!) 41 | } 42 | } 43 | 44 | showContent(currentFolder) 45 | } 46 | 47 | override fun onOptionsItemSelected(item: MenuItem): Boolean { 48 | when(item.itemId) { 49 | android.R.id.home -> { 50 | upOrFinish() 51 | return true 52 | } 53 | } 54 | return super.onOptionsItemSelected(item) 55 | } 56 | 57 | private fun upOrFinish() { 58 | if(currentFolder?.isRoot ?: true) { 59 | finish() 60 | return 61 | } 62 | showContent(currentFolder!!.parent) 63 | } 64 | 65 | override fun onBackPressed() { 66 | upOrFinish() 67 | } 68 | 69 | private fun openFile(item: ZipEntry) { 70 | val intent = Intent(this, SourceViewActivity::class.java) 71 | intent.putExtra("ZIP_FILE_ENTRY", item.toString()) 72 | intent.putExtra("LINE_NUM", 1) 73 | startActivity(intent) 74 | } 75 | 76 | 77 | private fun showContent(dir : ZipEntryAux?) { 78 | 79 | currentFolder = dir 80 | entries = zipArchive.listFilesAt(currentFolder) 81 | val adapter = ArrayAdapter(this, R.layout.list_item, entries.map { it.displayName }) 82 | listView.adapter = adapter 83 | 84 | supportActionBar!!.title = currentFolder?.name ?: "/" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/index/Main.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading.index 2 | 3 | import java.io.File 4 | 5 | fun main(args:Array) { 6 | var index = Index.open(File(args[0])) 7 | 8 | for (fileNo in index.postingList(args[1].trigram())) { 9 | println(index.readName(fileNo)) 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/index/Read.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading.index 2 | 3 | import com.livejournal.karino2.zipsourcecodereading.Query 4 | import com.livejournal.karino2.zipsourcecodereading.QueryOp 5 | import java.io.File 6 | import java.io.FileInputStream 7 | import java.nio.ByteBuffer 8 | import java.nio.channels.FileChannel 9 | 10 | // Copied From https://github.com/google/codesearch 11 | 12 | // An index stored on disk has the format: 13 | // 14 | // "csearch index 1\n" 15 | // list of paths 16 | // list of names 17 | // list of posting lists 18 | // name index 19 | // posting list index 20 | // trailer 21 | // 22 | // The list of paths is a sorted sequence of NUL-terminated file or directory names. 23 | // The index covers the file trees rooted at those paths. 24 | // The list ends with an empty name ("\x00"). 25 | // 26 | // The list of names is a sorted sequence of NUL-terminated file names. 27 | // The initial entry in the list corresponds to file #0, 28 | // the next to file #1, and so on. The list ends with an 29 | // empty name ("\x00"). 30 | // 31 | // The list of posting lists are a sequence of posting lists. 32 | // Each posting list has the form: 33 | // 34 | // trigram [3] 35 | // deltas [v]... 36 | // 37 | // The trigram gives the 3 byte trigram that this list describes. The 38 | // delta list is a sequence of varint-encoded deltas between file 39 | // IDs, ending with a zero delta. For example, the delta list [2,5,1,1,0] 40 | // encodes the file ID list 1, 6, 7, 8. The delta list [0] would 41 | // encode the empty file ID list, but empty posting lists are usually 42 | // not recorded at all. The list of posting lists ends with an entry 43 | // with trigram "\xff\xff\xff" and a delta list consisting a single zero. 44 | // 45 | // The indexes enable efficient random access to the lists. The name 46 | // index is a sequence of 4-byte big-endian values listing the byte 47 | // offset in the name list where each name begins. The posting list 48 | // index is a sequence of index entries describing each successive 49 | // posting list. Each index entry has the form: 50 | // 51 | // trigram [3] 52 | // file count [4] 53 | // offset [4] 54 | // 55 | // Index entries are only written for the non-empty posting lists, 56 | // so finding the posting list for a specific trigram requires a 57 | // binary search over the posting list index. In practice, the majority 58 | // of the possible trigrams are never seen, so omitting the missing 59 | // ones represents a significant storage savings. 60 | // 61 | // The trailer has the form: 62 | // 63 | // offset of path list [4] 64 | // offset of name list [4] 65 | // offset of posting lists [4] 66 | // offset of name index [4] 67 | // offset of posting list index [4] 68 | // "\ncsearch trailr\n" 69 | 70 | object Const { 71 | const val MAGIC = "csearch index 1\n" 72 | const val TRAILER_MAGIC = "\ncsearch trailr\n" 73 | 74 | const val POST_ENTRY_SIZE = 3 + 4 + 4 75 | 76 | const val MAX_FILE_LEN = 1 shl 30 77 | const val MAX_LINE_LEN = 2000 78 | const val MAX_TEXT_TRIGRAMS = 20000 79 | } 80 | 81 | fun Byte.toUint() = this.toInt() and 0xff 82 | 83 | fun String.trigram() : Int { 84 | val bytes = this.toByteArray() 85 | return (bytes[0].toUint() shl 16) or (bytes[1].toUint() shl 8) or bytes[2].toUint() 86 | } 87 | 88 | // An Index implements read-only access to a trigram index. 89 | class Index(val data: ByteBuffer, offsets : Int) { 90 | val pathData: Int = data.getInt(offsets) 91 | val nameData: Int = data.getInt(offsets + 4) 92 | val postData: Int = data.getInt(offsets + 8) 93 | val nameIndex: Int = data.getInt(offsets + 12) 94 | val postIndex: Int = data.getInt(offsets + 16) 95 | val numName: Int = (postIndex - nameIndex) / 4 - 1 96 | val numPost: Int = (offsets - postIndex) / Const.POST_ENTRY_SIZE 97 | 98 | companion object { 99 | fun open(file : File) : Index { 100 | val len = file.length() 101 | val data = 102 | FileInputStream(file).channel.map(FileChannel.MapMode.READ_ONLY, 0L, len) 103 | val offsets = len - Const.TRAILER_MAGIC.length - 5 * 4; 104 | if (offsets < 0 || offsets >= Int.MAX_VALUE) { 105 | throw IllegalArgumentException(file.toString()) 106 | } 107 | 108 | return Index(data, offsets.toInt()) 109 | } 110 | } 111 | 112 | fun readInt32(offset : Int) : Int = data.getInt(offset) 113 | 114 | fun readString(offset: Int) : String = 115 | buildString { 116 | var i = 0 117 | while (data[offset + i] != 0.toByte()) { 118 | append(data[offset + i].toChar()) 119 | i++ 120 | } 121 | } 122 | 123 | fun readPaths(): List { 124 | var off = pathData 125 | val paths = mutableListOf() 126 | while(true) { 127 | val s = readString(off) 128 | if (s.isEmpty()) break; 129 | paths.add(s) 130 | off += s.length + 1 131 | } 132 | return paths 133 | } 134 | 135 | fun readName(fieldId: Int) : String { 136 | var off = readInt32(nameIndex + 4 * fieldId) 137 | return readString(nameData + off) 138 | } 139 | 140 | fun readListAt(off: Int) { 141 | // TODO: 142 | } 143 | 144 | fun readTrigram(off : Int) : Int = 145 | (data[off].toUint() shl 16) or (data[off + 1].toUint() shl 8) or (data[off + 2].toUint()) 146 | 147 | fun createPostReader(trigram: Int) : PostReader? { 148 | val ix = getPostIndex(trigram) ?: return null 149 | val off = postIndex + ix * Const.POST_ENTRY_SIZE 150 | val count = readInt32(off + 3) 151 | val offset = postData + readInt32(off + 7) + 3 152 | // println(String.format("%06x: %d at %d (%x)", readTrigram(off), count, offset, offset)) 153 | if (readTrigram(off) != trigram) return null 154 | return PostReader(offset, count) 155 | } 156 | 157 | private fun getPostIndex(trigram: Int): Int? { 158 | // TODO: use binary search 159 | return (0 until numPost).firstOrNull { 160 | readTrigram(postIndex + it * Const.POST_ENTRY_SIZE) == trigram 161 | } 162 | } 163 | 164 | inner class PostReader(var offset: Int, var count: Int) { 165 | var fileId :Int = -1 166 | 167 | fun max() = count 168 | 169 | fun next() : Boolean { 170 | count-- 171 | val delta = readVarInt() 172 | if (delta == 0) { 173 | fileId = -1 174 | return false 175 | } 176 | fileId += delta 177 | return true 178 | } 179 | 180 | fun readVarInt() : Int { 181 | var res : Int = 0 182 | var s = 0 183 | do { 184 | val b = data.get(offset).toInt() 185 | res = res or ((b and 0x7f) shl s) 186 | offset++ 187 | s+=7 188 | } while ((b and 0x80) != 0) 189 | return res 190 | } 191 | 192 | } 193 | 194 | fun postingList(trigram: Int): Collection { 195 | val r = createPostReader(trigram) 196 | val res = mutableSetOf() 197 | if (r != null) { 198 | while (r.next()) { 199 | val fileId = r.fileId 200 | res.add(fileId) 201 | } 202 | } 203 | return res 204 | } 205 | 206 | // postingAnd and postingOr can be replaced with set operations 207 | 208 | fun postingQuery(q : Query) : Collection { 209 | val res = mutableSetOf() 210 | when (q.Op) { 211 | QueryOp.NONE -> return emptySet() 212 | QueryOp.ALL -> { 213 | res += 0 until numName 214 | } 215 | QueryOp.AND -> { 216 | var first = true 217 | for (s in q.Trigram) { 218 | if (first) { 219 | res += postingList(s.trigram()) 220 | first = false 221 | } else { 222 | res.retainAll(postingList(s.trigram())) 223 | } 224 | if (res.isEmpty()) { 225 | return emptySet() 226 | } 227 | } 228 | for (s in q.Sub) { 229 | res.retainAll(postingQuery(s)) 230 | if (res.isEmpty()) { 231 | return emptySet() 232 | } 233 | } 234 | } 235 | QueryOp.OR -> { 236 | for (s in q.Trigram) { 237 | res += postingList(s.trigram()) 238 | } 239 | for (s in q.Sub) { 240 | res += postingQuery(s) 241 | } 242 | } 243 | } 244 | return res 245 | } 246 | 247 | fun dumpPosting() { 248 | for (i in 0 until numPost) { 249 | val j = postIndex + i * Const.POST_ENTRY_SIZE 250 | val t = readTrigram(j) 251 | val count = readInt32(j + 3) 252 | val offset = readInt32(j + 7) 253 | println(String.format("%06x: %d at %d (%x)", t, count, offset, offset)) 254 | } 255 | } 256 | } -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/index/Write.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading.index 2 | 3 | import java.io.* 4 | 5 | data class PostEntry(val trigram : Int, val fileId: Int) : Comparable { 6 | override fun compareTo(other: PostEntry): Int = 7 | if (other.trigram != trigram) 8 | trigram.compareTo(other.trigram) 9 | else 10 | fileId.compareTo(other.fileId) 11 | } 12 | 13 | fun DataOutput.writeUtf8(s : String) { 14 | write(s.toByteArray(charset = Charsets.UTF_8)) 15 | } 16 | 17 | fun DataOutput.writeTrigram(trigram: Int) { 18 | writeByte((trigram shr 16) and 0xff) 19 | writeByte((trigram shr 8) and 0xff) 20 | writeByte(trigram and 0xff) 21 | } 22 | 23 | fun DataOutput.writeVarint(n: Int) { 24 | var v = n 25 | while (v >= 0x80) { 26 | writeByte((v and 0x7f) or 0x80) 27 | v = v shr 7 28 | } 29 | writeByte(v) 30 | } 31 | 32 | class IndexException(message: String) : RuntimeException(message) 33 | 34 | class IndexWriter( 35 | val tmpDir : File, 36 | outputFile: File, 37 | val postMax : Int = (64 shl 20) / 8 // 64MiB worth of posts entries 38 | ) { 39 | val paths = mutableListOf() 40 | 41 | val nameDataFile = createTempFile("nameData") 42 | val nameData = DataOutputStream(BufferedOutputStream(FileOutputStream(nameDataFile))) 43 | val nameIndexFile = createTempFile("nameIndex") 44 | val nameIndex = DataOutputStream(BufferedOutputStream(FileOutputStream(nameIndexFile))) 45 | var numName = 0 46 | var totalBytes = 0L 47 | 48 | val posts = mutableListOf() 49 | val postFiles = mutableListOf() 50 | val postIndexFile = createTempFile("postIndex") 51 | 52 | val main = DataOutputStream(BufferedOutputStream(FileOutputStream(outputFile))) 53 | 54 | private fun createTempFile(type: String) : File = 55 | File.createTempFile(type + "-", ".tmp", tmpDir) 56 | 57 | fun addPaths(paths : List) { 58 | this.paths += paths 59 | } 60 | 61 | fun addFile(name: File) { 62 | FileInputStream(name).use { 63 | add(name.toString(), DataInputStream(BufferedInputStream(it))) 64 | } 65 | } 66 | 67 | fun add(name: String, inputStream: DataInput) { 68 | val trigrams = mutableSetOf() 69 | var tv = 0 70 | var fileLen = 0L 71 | var lineLen = 0 72 | try { 73 | while (true) { 74 | tv = (tv shl 8) and 0xffffff 75 | 76 | var c = inputStream.readByte() 77 | tv = tv or c.toUint() 78 | fileLen++ 79 | if (fileLen >= 3) { 80 | trigrams.add(tv) 81 | } 82 | if (!validUtf8((tv shr 8) and 0xff, tv and 0xff)) { 83 | throw IndexException(String.format("%s: invalid UTF-8, ignoring", name)) 84 | } 85 | 86 | if (fileLen > Const.MAX_FILE_LEN) { 87 | throw IndexException(String.format("%s: too long, ignoring", name)) 88 | } 89 | lineLen++ 90 | if (lineLen > Const.MAX_LINE_LEN) { 91 | throw IndexException(String.format("%s: very long lines, ignoring", name)) 92 | } 93 | if (c == '\n'.toByte()) { 94 | lineLen = 0 95 | } 96 | } 97 | } catch (e: EOFException) { 98 | // Ignore 99 | } 100 | 101 | if (trigrams.size > Const.MAX_TEXT_TRIGRAMS) { 102 | throw IndexException(String.format("%s: too many trigrams, probably not text, ignoring", name)) 103 | } 104 | totalBytes += fileLen 105 | 106 | val fileId = addName(name) 107 | 108 | // TODO: Log 109 | 110 | for (t in trigrams) { 111 | if (posts.size >= postMax) { 112 | flushPost() 113 | } 114 | posts += PostEntry(t, fileId) 115 | } 116 | } 117 | 118 | private fun addName(name: String) : Int { 119 | nameIndex.writeInt(nameData.size()) 120 | nameData.writeUtf8(name) 121 | nameData.writeByte(0) 122 | return numName++ 123 | } 124 | 125 | // flushes the index entry to the target file. 126 | fun flush() { 127 | addName("") 128 | 129 | nameData.close() 130 | nameIndex.close() 131 | 132 | val offs = mutableListOf() 133 | 134 | main.writeUtf8(Const.MAGIC) 135 | offs += main.size() 136 | for (s in paths) { 137 | main.writeUtf8(s) 138 | main.write(0) 139 | } 140 | main.write(0) 141 | offs += main.size() 142 | copyFile(main, nameDataFile) 143 | offs += main.size() 144 | mergePost(main) 145 | offs += main.size() 146 | copyFile(main, nameIndexFile) 147 | offs += main.size() 148 | copyFile(main, postIndexFile) 149 | 150 | for (off in offs) { 151 | main.writeInt(off) 152 | } 153 | main.writeUtf8(Const.TRAILER_MAGIC) 154 | main.close() 155 | 156 | nameDataFile.delete() 157 | nameIndexFile.delete() 158 | postIndexFile.delete() 159 | postFiles.forEach { it.delete() } 160 | } 161 | 162 | private fun copyFile(dest: OutputStream, src: File) { 163 | val buf = ByteArray(1 shl 14) 164 | src.inputStream().use { 165 | while (true) { 166 | val n = it.read(buf) 167 | if (n <= 0) break 168 | dest.write(buf, 0, n) 169 | } 170 | } 171 | } 172 | 173 | private fun flushPost() { 174 | val tempIndexFile = File.createTempFile("postEntry-", ".idx", tmpDir) 175 | posts.sortBy { it.trigram } 176 | DataOutputStream(BufferedOutputStream(tempIndexFile.outputStream())).use { 177 | out -> 178 | for (post in posts) { 179 | out.writeInt(post.trigram) 180 | out.writeInt(post.fileId) 181 | } 182 | } 183 | this.posts.clear() 184 | postFiles += tempIndexFile 185 | } 186 | 187 | private fun mergePost(out: DataOutputStream) { 188 | val heap = PostHeap() 189 | 190 | for (f in postFiles) { 191 | val entries = FiledPostEntries(f) 192 | if (entries.hasNext()) { 193 | heap.add(PostChunk(entries)) 194 | } 195 | } 196 | if (posts.isNotEmpty()) { 197 | posts.sortBy { it.trigram } 198 | heap.add(PostChunk(posts.iterator())) 199 | } 200 | 201 | DataOutputStream(BufferedOutputStream(FileOutputStream(postIndexFile))).use { 202 | postIndex -> 203 | 204 | var nPost = 0 205 | val offset0 = out.size() 206 | var entry = heap.next() 207 | do { 208 | nPost++ 209 | val offset = out.size() - offset0 210 | val trigram = entry.trigram 211 | 212 | out.writeTrigram(trigram) 213 | 214 | var fileId = -1 215 | var nFile = 0; 216 | 217 | do { 218 | out.writeVarint(entry.fileId - fileId) 219 | fileId = entry.fileId 220 | nFile++ 221 | if (!heap.hasNext()) break; 222 | entry = heap.next() 223 | } while (entry.trigram == trigram) 224 | out.writeVarint(0) 225 | 226 | postIndex.writeTrigram(trigram) 227 | postIndex.writeInt(nFile) 228 | postIndex.writeInt(offset) 229 | } while (heap.hasNext()) 230 | } 231 | } 232 | } 233 | 234 | class FiledPostEntries(val stream: DataInputStream) : Iterator { 235 | var next : PostEntry? = prepareNext() 236 | 237 | constructor(file: File) : 238 | this(DataInputStream(BufferedInputStream(FileInputStream(file)))) { 239 | } 240 | 241 | override fun hasNext(): Boolean = next != null 242 | 243 | override fun next(): PostEntry { 244 | val ret = next!! 245 | prepareNext() 246 | return ret 247 | } 248 | 249 | private fun prepareNext(): PostEntry? { 250 | try { 251 | val trigram = stream.readInt() 252 | val fileId = stream.readInt() 253 | next = PostEntry(trigram = trigram, fileId = fileId) 254 | } catch (e: EOFException) { 255 | next = null 256 | } 257 | return next 258 | } 259 | } 260 | 261 | class PostChunk(val m: Iterator) : Comparable { 262 | var e: PostEntry = m.next() 263 | override fun compareTo(other: PostChunk) = e.compareTo(other.e) 264 | } 265 | 266 | class PostHeap : Iterator { 267 | private val chunks = java.util.TreeSet() 268 | 269 | fun add(ch: PostChunk) = chunks.add(ch) 270 | 271 | override fun next() : PostEntry { 272 | val ch = chunks.pollFirst() 273 | val entry = ch.e 274 | if (ch.m.hasNext()) { 275 | ch.e = ch.m.next() 276 | chunks.add(ch) 277 | } 278 | return entry 279 | } 280 | 281 | override fun hasNext() : Boolean = !chunks.isEmpty() 282 | } 283 | 284 | fun validUtf8(c1: Int, c2: Int) : Boolean = 285 | when (c1) { 286 | in 0 until 0x80 -> 287 | // 1-byte, must be followed by 1-byte or first of multi-byte 288 | c2 < 0x80 || c2 in 0xc0 until 0xf8 289 | in 0x80 until 0xc0 -> 290 | // continuation byte, can be followed by nearly anything 291 | c2 < 0xf8 292 | in 0xc0 until 0xf8 -> 293 | // first of multi-byte, must be followed by continuation byte 294 | c2 in 0x80 until 0xc0 295 | else -> 296 | false 297 | } -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/text/ArrayUtils.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading.text 2 | 3 | /** 4 | * Created by _ on 2017/10/03. 5 | */ 6 | class ArrayUtils { 7 | companion object { 8 | fun idealByteArraySize(need: Int): Int { 9 | for (i in 4..31) 10 | if (need <= (1 shl i) - 12) 11 | return (1 shl i) - 12 12 | 13 | return need 14 | } 15 | 16 | fun idealIntArraySize(need: Int): Int { 17 | return idealByteArraySize(need * 4) / 4 18 | } 19 | 20 | fun idealCharArraySize(need: Int): Int { 21 | return idealByteArraySize(need * 2) / 2 22 | } 23 | 24 | 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/text/FastScroller.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading.text 2 | 3 | import android.content.Context 4 | import android.graphics.Canvas 5 | import android.graphics.Paint 6 | import android.graphics.drawable.Drawable 7 | import android.os.SystemClock 8 | import android.util.DisplayMetrics 9 | import android.util.Log 10 | import android.view.MotionEvent 11 | import android.view.WindowManager 12 | import com.livejournal.karino2.zipsourcecodereading.R 13 | 14 | /** 15 | * Created by _ on 2017/10/04. 16 | */ 17 | class FastScroller(val target: LongTextView) { 18 | enum class State { 19 | NONE, ENTER, VISIBLE, DRAGGING, EXIT 20 | } 21 | 22 | val thumbDrawable : Drawable by lazy { 23 | target.resources.getDrawable(R.drawable.scrollbar_handle) 24 | } 25 | 26 | val dpi : Float by lazy { 27 | val dm = DisplayMetrics() 28 | val wm = target.context.getSystemService(Context.WINDOW_SERVICE) as WindowManager 29 | wm.defaultDisplay.getMetrics(dm) 30 | (dm.xdpi+dm.ydpi)/2 31 | } 32 | 33 | val thumbH : Int by lazy { 34 | ((48*dpi)/160).toInt() 35 | } 36 | val thumbW : Int by lazy { 37 | ((52*dpi)/160).toInt() 38 | } 39 | var thumbY = 0 40 | 41 | var state = State.NONE 42 | 43 | fun gotoState(newState: State) { 44 | when(newState) { 45 | State.NONE -> { 46 | target.removeCallbacks(scrollFade) 47 | target.invalidate() 48 | } 49 | State.VISIBLE -> { 50 | if(state != State.VISIBLE) { 51 | resetThumbPos() 52 | } 53 | target.removeCallbacks(scrollFade) 54 | } 55 | State.DRAGGING -> { 56 | target.removeCallbacks(scrollFade) 57 | } 58 | State.EXIT -> { 59 | invalidateSelf() 60 | } 61 | } 62 | state = newState 63 | } 64 | 65 | fun resetThumbPos() { 66 | val vw = target.getWidth() 67 | thumbDrawable.setBounds(vw - thumbW, 0, vw, thumbH); 68 | thumbDrawable.setAlpha(ALPHA_MAX); 69 | 70 | } 71 | 72 | val ALPHA_MAX = 200 73 | 74 | val paint : Paint by lazy { 75 | val pai = Paint() 76 | pai.setAntiAlias(true) 77 | pai.setTextAlign(Paint.Align.CENTER) 78 | pai 79 | } 80 | 81 | fun invalidateSelf() { 82 | val vw = target.getWidth() 83 | target.invalidate(vw - thumbW, thumbY, vw, thumbY+thumbH) 84 | } 85 | 86 | fun stop() = gotoState(State.NONE) 87 | 88 | val isVisible : Boolean 89 | get() = state != State.NONE 90 | 91 | fun draw(canvas : Canvas) { 92 | if(state == State.NONE) 93 | return 94 | val y = (thumbY + target.scrollY).toFloat() 95 | val viewWidth = target.width 96 | val x = target.scrollX.toFloat() 97 | 98 | var alpha = -1 99 | if(state == State.EXIT) { 100 | alpha = scrollFade.alpha 101 | if(alpha < ALPHA_MAX/2) { 102 | thumbDrawable.setAlpha(alpha*2) 103 | } 104 | 105 | } 106 | 107 | canvas.translate(x, y) 108 | thumbDrawable.draw(canvas) 109 | canvas.translate(-x, -y) 110 | 111 | if(alpha == 0) { 112 | gotoState(State.NONE) 113 | } else { 114 | // I think this must be the same as invalidateSelf(), invalidate self does not have scrollY, and here is. Which is correct? 115 | target.invalidate(viewWidth - thumbW, y.toInt(), viewWidth, (y+thumbH).toInt()) 116 | } 117 | } 118 | 119 | 120 | fun onSizeChanged(w: Int, h:Int, oldW:Int, oldH : Int) { 121 | thumbDrawable.setBounds(w-thumbW, 0, w, thumbH) 122 | } 123 | 124 | var scrollCompleted = true 125 | var currentVScrollOrigin = -1 126 | 127 | fun onScroll(vscrollOrigin : Int, visibleHeight: Int, wholeHeight : Int) { 128 | if(visibleHeight <= 0) 129 | return 130 | 131 | if(wholeHeight - visibleHeight > 0 && state != State.DRAGGING ) { 132 | thumbY = ((visibleHeight - thumbH)*vscrollOrigin)/(wholeHeight- visibleHeight) 133 | } 134 | scrollCompleted = true 135 | if(currentVScrollOrigin == vscrollOrigin) 136 | return 137 | 138 | currentVScrollOrigin = vscrollOrigin 139 | if(state != State.DRAGGING) { 140 | gotoState(State.VISIBLE) 141 | target.postDelayed(scrollFade, 1500) 142 | } 143 | 144 | } 145 | 146 | fun isPointInside(x: Float, y: Float) = x > target.width - thumbW && y >= thumbY && y <= thumbY+thumbH 147 | 148 | 149 | var lastEventTime = 0L 150 | 151 | fun onTouchEvent(ev : MotionEvent) : Boolean { 152 | if(state == State.NONE) 153 | return false 154 | 155 | val x = ev.x 156 | val y = ev.y 157 | when(ev.action) { 158 | MotionEvent.ACTION_DOWN -> { 159 | if(isPointInside(x, y)) { 160 | gotoState(State.DRAGGING) 161 | return true 162 | } 163 | return false 164 | } 165 | MotionEvent.ACTION_UP -> { 166 | if(state == State.DRAGGING) { 167 | gotoState(State.VISIBLE) 168 | target.removeCallbacks(scrollFade) 169 | target.postDelayed(scrollFade, 1000) 170 | return true 171 | } 172 | return false 173 | } 174 | MotionEvent.ACTION_MOVE -> { 175 | if(state != State.DRAGGING) 176 | return false 177 | 178 | val now = System.currentTimeMillis() 179 | val diff = now - lastEventTime 180 | if(diff > 30) { 181 | lastEventTime = now 182 | val viewHeight = target.height 183 | val newThumbY = Math.min(Math.max(0, (ev.y - thumbH/2).toInt()), viewHeight - thumbH) 184 | if(Math.abs(newThumbY - thumbY) < 2) 185 | return true 186 | 187 | thumbY = newThumbY 188 | scrollTo(thumbY.toFloat()/(viewHeight - thumbH)) 189 | return true 190 | 191 | } 192 | return true 193 | 194 | } 195 | else -> return false 196 | } 197 | } 198 | 199 | fun scrollTo(pos : Float) { 200 | target.moveToVLineNow((target.lineCount*pos).toInt()) 201 | } 202 | 203 | 204 | inner class ScrollFade : Runnable { 205 | var startTime = 0L 206 | val FADE_DURATION = 200L 207 | 208 | fun startFade() { 209 | startTime = SystemClock.uptimeMillis() 210 | gotoState(State.EXIT) 211 | } 212 | 213 | val alpha : Int 214 | get() { 215 | if(state != State.EXIT) 216 | return ALPHA_MAX 217 | val dur = SystemClock.uptimeMillis() - startTime 218 | 219 | if(dur > FADE_DURATION) { 220 | return 0 221 | } 222 | return (ALPHA_MAX - (dur*ALPHA_MAX)/FADE_DURATION).toInt() 223 | } 224 | 225 | 226 | 227 | override fun run() { 228 | if(state != State.EXIT) { 229 | startFade() 230 | return 231 | } 232 | if(alpha > 0) { 233 | invalidateSelf() 234 | } else { 235 | gotoState(State.NONE) 236 | } 237 | } 238 | 239 | } 240 | 241 | val scrollFade = ScrollFade() 242 | 243 | 244 | 245 | } -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/text/HandleView.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading.text 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Rect 5 | import android.view.* 6 | import android.widget.PopupWindow 7 | 8 | 9 | /** 10 | * Created by _ on 2017/10/05. 11 | */ 12 | abstract class HandleView(val parent: LongTextView, pos: HandleSide) : View(parent.context) { 13 | enum class HandleSide { 14 | LEFT, 15 | RIGHT 16 | } 17 | 18 | val drawable by lazy { 19 | when(pos) { 20 | HandleSide.LEFT -> parent.handleLeftDrawable 21 | HandleSide.RIGHT -> parent.handleRightDrawable 22 | } 23 | } 24 | private val container: PopupWindow by lazy { 25 | val pop = PopupWindow(parent.context, null, 26 | android.R.attr.textSelectHandleWindowStyle) 27 | pop.setClippingEnabled(false) 28 | pop 29 | } 30 | 31 | private var positionX: Int = 0 32 | private var positionY: Int = 0 33 | 34 | var isDragging: Boolean = false 35 | private set 36 | 37 | private var touchToWindowOffsetX: Float = 0.toFloat() 38 | private var touchToWindowOffsetY: Float = 0.toFloat() 39 | 40 | val hotspotX: Float by lazy { 41 | when(pos) { 42 | HandleSide.LEFT -> { 43 | val handleWidth = drawable.intrinsicWidth 44 | (handleWidth * 3 / 4).toFloat() 45 | } 46 | HandleSide.RIGHT -> { 47 | val handleWidth = drawable.intrinsicWidth 48 | (handleWidth / 4).toFloat() 49 | } 50 | } 51 | } 52 | 53 | 54 | val hotspotY = 0f 55 | val _height by lazy { drawable.intrinsicHeight } 56 | val touchOffsetY: Float by lazy { -drawable.intrinsicHeight * 0.3f } 57 | private var lastParentX: Int = 0 58 | private var lastParentY: Int = 0 59 | 60 | val isShowing: Boolean 61 | get() = container.isShowing 62 | 63 | val tempRect = Rect() 64 | var tempCoords = IntArray(2) 65 | 66 | private 67 | val isPositionVisible: Boolean 68 | get() { 69 | if (isDragging) { 70 | return true 71 | } 72 | 73 | val extendedPaddingTop = parent.paddingTop 74 | val extendedPaddingBottom = parent.paddingBottom 75 | val compoundPaddingLeft = parent.paddingLeft 76 | val compoundPaddingRight = parent.paddingRight 77 | 78 | val hostView = parent 79 | val left = 0 80 | val right = hostView.width 81 | val top = 0 82 | val bottom = hostView.height 83 | 84 | val clip = tempRect 85 | clip.left = left + compoundPaddingLeft 86 | clip.top = top + extendedPaddingTop 87 | clip.right = right - compoundPaddingRight 88 | clip.bottom = bottom - extendedPaddingBottom 89 | 90 | val _parent = hostView.parent 91 | if (_parent == null || !_parent!!.getChildVisibleRect(hostView, clip, null)) { 92 | return false 93 | } 94 | 95 | val coords = tempCoords 96 | hostView.getLocationInWindow(coords) 97 | val posX = coords[0] + positionX + hotspotX.toInt() 98 | val posY = coords[1] + positionY + hotspotY.toInt() 99 | 100 | return posX >= clip.left && posX <= clip.right && 101 | posY >= clip.top && posY <= clip.bottom 102 | } 103 | 104 | init { 105 | invalidate() 106 | } 107 | 108 | 109 | 110 | override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { 111 | setMeasuredDimension(drawable!!.intrinsicWidth, 112 | drawable!!.intrinsicHeight) 113 | } 114 | 115 | fun show() { 116 | if (!isPositionVisible) { 117 | hide() 118 | return 119 | } 120 | container.setContentView(this) 121 | val coords = tempCoords 122 | parent.getLocationInWindow(coords) 123 | coords[0] += positionX 124 | coords[1] += positionY 125 | container.showAtLocation(parent, Gravity.NO_GRAVITY, coords[0], coords[1]) 126 | } 127 | 128 | fun hide() { 129 | isDragging = false 130 | container.dismiss() 131 | } 132 | 133 | private fun moveTo(x: Int, y: Int) { 134 | positionX = x - parent.scrollX 135 | positionY = y - parent.scrollY 136 | if (isPositionVisible) { 137 | var coords: IntArray? = null 138 | if (container.isShowing()) { 139 | coords = tempCoords 140 | parent.getLocationInWindow(coords) 141 | container.update(coords!![0] + positionX, coords[1] + positionY, 142 | parent.right - parent.left, parent.bottom - parent.top) 143 | } else { 144 | show() 145 | } 146 | 147 | if (isDragging) { 148 | if (coords == null) { 149 | coords = tempCoords 150 | parent.getLocationInWindow(coords) 151 | } 152 | if (coords!![0] != lastParentX || coords[1] != lastParentY) { 153 | touchToWindowOffsetX += (coords[0] - lastParentX).toFloat() 154 | touchToWindowOffsetY += (coords[1] - lastParentY).toFloat() 155 | lastParentX = coords[0] 156 | lastParentY = coords[1] 157 | } 158 | } 159 | } else { 160 | hide() 161 | } 162 | } 163 | 164 | override fun onDraw(c: Canvas) { 165 | // drawable!!.setBounds(0, 0, parent.right - parent.left, parent.bottom - parent.top) 166 | drawable!!.setBounds(0, 0, drawable!!.intrinsicWidth, drawable!!.intrinsicHeight) 167 | drawable!!.draw(c) 168 | } 169 | 170 | override fun onTouchEvent(ev: MotionEvent): Boolean { 171 | when (ev.action and MotionEvent.ACTION_MASK) { 172 | MotionEvent.ACTION_DOWN -> { 173 | val rawX = ev.rawX 174 | val rawY = ev.rawY 175 | touchToWindowOffsetX = rawX - positionX 176 | touchToWindowOffsetY = rawY - positionY 177 | val coords = tempCoords 178 | parent.getLocationInWindow(coords) 179 | lastParentX = coords[0] 180 | lastParentY = coords[1] 181 | isDragging = true 182 | } 183 | 184 | MotionEvent.ACTION_MOVE -> { 185 | val rawX = ev.rawX 186 | val rawY = ev.rawY 187 | val newPosX = rawX - touchToWindowOffsetX + hotspotX - parent.lineNumberWidth 188 | val newPosY = rawY - touchToWindowOffsetY + hotspotY + touchOffsetY 189 | 190 | updatePosition(this, Math.round(newPosX+offsetX), Math.round(newPosY)) 191 | } 192 | 193 | MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> isDragging = false 194 | } 195 | return true 196 | } 197 | 198 | abstract fun updatePosition(handleView: HandleView, x: Int, y: Int) 199 | 200 | val layout: Layout 201 | get() = parent.layout!! 202 | 203 | fun positionAtCursor(offset: Int, bottom: Boolean) { 204 | val width = drawable.intrinsicWidth 205 | val height = drawable.intrinsicHeight 206 | val line = layout.getLineForOffset(offset) 207 | val lineTop = layout.getLineTop(line) 208 | val lineBottom = layout.getLineBottom(line) 209 | 210 | val bounds = tempRect 211 | bounds.left = ((layout.getPrimaryHorizontal(offset) - hotspotX).toInt() 212 | + parent.scrollX + parent.lineNumberWidth) 213 | bounds.top = (if (bottom) lineBottom else lineTop - _height) + parent.scrollY 214 | 215 | bounds.right = bounds.left + width 216 | bounds.bottom = bounds.top + height 217 | 218 | parent.convertFromViewportToContentCoordinates(bounds) 219 | moveTo(bounds.left+offsetX, bounds.top) 220 | } 221 | val offsetX = -7 222 | } -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/text/README.md: -------------------------------------------------------------------------------- 1 | # LongTextView 2 | 3 | This folder contains implementation of LongTextView. 4 | 5 | Standard TextView is designed for short text, and supporting warap_content make layout heavy task. 6 | 7 | For source code reading, source code is basically long. 8 | And size of TextView is specified by parent, and do not need to layout by themselves. 9 | Also, showing only visible area is important for fast syntax coloring. 10 | 11 | This LongTextView is designed for Long source code file reading only. 12 | 13 | This impolementation is similar to https://github.com/githubth/jota-text-editor design (which is also similar to Android TextView), but there implementation make TextView class similar to Android TextView class, and it makes implementation complex. 14 | I only support read-only textview with text always SpannableString, because this is the only usecase for us. 15 | So our Layout is only StaticLayout, I merge Layout class and StaticLayout class. 16 | 17 | Also, I do not support emoji, bidi (RTL), hint. 18 | This make our code much smaller. -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/text/SelectionController.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading.text 2 | 3 | import android.os.SystemClock 4 | import android.view.MotionEvent 5 | import android.view.ViewConfiguration 6 | import android.content.ContentValues.TAG 7 | import android.text.Selection 8 | import android.util.Log 9 | import android.view.ViewTreeObserver 10 | 11 | 12 | /** 13 | * Created by _ on 2017/10/05. 14 | */ 15 | class SelectionController(val parent: LongTextView) : ViewTreeObserver.OnTouchModeChangeListener { 16 | // The cursor controller images 17 | private val mStartHandle by lazy { 18 | object : HandleView(parent, HandleView.HandleSide.LEFT) { 19 | override fun updatePosition(handleView: HandleView, x: Int, y: Int) { 20 | _updatePosition(handleView, x, y) 21 | } 22 | 23 | } 24 | } 25 | 26 | 27 | private val mEndHandle by lazy { 28 | object : HandleView(parent, HandleView.HandleSide.RIGHT) { 29 | override fun updatePosition(handleView: HandleView, x: Int, y: Int) { 30 | _updatePosition(handleView, x, y) 31 | } 32 | } 33 | } 34 | 35 | var lastTouchOffset: Int = 0 36 | // Whether selection anchors are active 37 | var isShowing: Boolean = false 38 | private set 39 | // Double tap detection 40 | private var mPreviousTapUpTime: Long = 0 41 | var lastTapPositionX: Int = 0 42 | var lastTapPositionY: Int = 0 43 | 44 | /** 45 | * @return true iff this controller is currently used to move the selection start. 46 | */ 47 | val isSelectionStartDragged: Boolean 48 | get() = mStartHandle.isDragging 49 | 50 | init { 51 | resetTouchOffsets() 52 | } 53 | 54 | fun show() { 55 | isShowing = true 56 | updatePosition() 57 | mStartHandle.show() 58 | mEndHandle.show() 59 | } 60 | 61 | fun hide() { 62 | mStartHandle.hide() 63 | mEndHandle.hide() 64 | isShowing = false 65 | } 66 | 67 | fun _updatePosition(handle: HandleView, x: Int, y: Int) { 68 | var selectionStart = parent.selectionStart 69 | var selectionEnd = parent.selectionEnd 70 | 71 | val previousOffset = if (handle === mStartHandle) selectionStart else selectionEnd 72 | var offset = parent.getHysteresisOffset(x, y, previousOffset) 73 | 74 | // Handle the case where start and end are swapped, making sure start <= end 75 | if (handle === mStartHandle) { 76 | if (selectionStart == offset || offset > selectionEnd) { 77 | return // no change, no need to redraw; 78 | } 79 | // If the user "closes" the selection entirely they were probably trying to 80 | // select a single character. Help them out. 81 | if (offset == selectionEnd) { 82 | offset = selectionEnd - 1 83 | } 84 | selectionStart = offset 85 | } else { 86 | if (selectionEnd == offset || offset < selectionStart) { 87 | return // no change, no need to redraw; 88 | } 89 | // If the user "closes" the selection entirely they were probably trying to 90 | // select a single character. Help them out. 91 | if (offset == selectionStart) { 92 | offset = selectionStart + 1 93 | } 94 | selectionEnd = offset 95 | } 96 | 97 | Selection.setSelection(parent.text, selectionStart, selectionEnd) 98 | updatePosition() 99 | } 100 | 101 | fun updatePosition() { 102 | if (!isShowing) { 103 | return 104 | } 105 | 106 | val selectionStart = parent.selectionStart 107 | val selectionEnd = parent.selectionEnd 108 | 109 | if (selectionStart < 0 || selectionEnd < 0) { 110 | // Should never happen, safety check. 111 | Log.w(TAG, "Update selection controller position called with no cursor") 112 | hide() 113 | return 114 | } 115 | 116 | mStartHandle.positionAtCursor(selectionStart, true) 117 | mEndHandle.positionAtCursor(selectionEnd, true) 118 | } 119 | 120 | fun onTouchEvent(event: MotionEvent): Boolean { 121 | // This is done even when the View does not have focus, so that long presses can start 122 | // selection and tap can move cursor from this tap position. 123 | // if (isTextEditable()) { 124 | when (event.action and MotionEvent.ACTION_MASK) { 125 | MotionEvent.ACTION_DOWN -> { 126 | val x = event.x.toInt() - parent.lineNumberWidth 127 | val y = event.y.toInt() 128 | 129 | // Remember finger down position, to be able to start selection from there 130 | lastTouchOffset = parent.getOffset(x, y) 131 | 132 | // Double tap detection 133 | val duration = SystemClock.uptimeMillis() - mPreviousTapUpTime 134 | if (duration <= ViewConfiguration.getDoubleTapTimeout()) { 135 | val deltaX = x - lastTapPositionX 136 | val deltaY = y - lastTapPositionY 137 | val distanceSquared = deltaX * deltaX + deltaY * deltaY 138 | val doubleTapSlop = ViewConfiguration.get(parent.context).scaledDoubleTapSlop 139 | val slopSquared = doubleTapSlop * doubleTapSlop 140 | if (distanceSquared < slopSquared) { 141 | parent.startSelectionActionMode() 142 | } 143 | } 144 | lastTapPositionX = x 145 | lastTapPositionY = y 146 | } 147 | 148 | MotionEvent.ACTION_UP -> mPreviousTapUpTime = SystemClock.uptimeMillis() 149 | } 150 | return false 151 | } 152 | 153 | fun resetTouchOffsets() { 154 | lastTouchOffset = -1 155 | } 156 | 157 | override fun onTouchModeChanged(isInTouchMode: Boolean) { 158 | if (!isInTouchMode) { 159 | hide() 160 | } 161 | } 162 | 163 | fun onDetached() {} 164 | } 165 | -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/text/Styled.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading.text 2 | 3 | import android.graphics.Canvas 4 | import android.graphics.Paint 5 | import android.text.* 6 | import android.text.style.CharacterStyle 7 | import android.text.style.MetricAffectingSpan 8 | import android.text.style.ReplacementSpan 9 | 10 | 11 | /** 12 | * Created by _ on 2017/10/03. 13 | */ 14 | class Styled { 15 | companion object { 16 | fun drawText(canvas: Canvas, text: Spannable, start: Int, end: Int, x: Float, top: Int, y: Float, bottom: Int, textPaint: TextPaint, workPaint: TextPaint, needWidth: Boolean) : Float { 17 | return drawDirectionalRun(canvas, text, start, end, 18 | x, top, y, bottom, null, textPaint, workPaint, 19 | needWidth) 20 | } 21 | 22 | private fun drawDirectionalRun(canvas: Canvas?, 23 | text: Spannable, start: Int, end: Int, 24 | x: Float, top: Int, y: Float, bottom: Int, 25 | fmi: Paint.FontMetricsInt?, 26 | paint: TextPaint, 27 | workPaint: TextPaint, 28 | needWidth: Boolean): Float { 29 | var x = x 30 | 31 | val ox = x 32 | var minAscent = 0 33 | var maxDescent = 0 34 | var minTop = 0 35 | var maxBottom = 0 36 | 37 | val division: Class<*> 38 | 39 | if (canvas == null) 40 | division = MetricAffectingSpan::class.java 41 | else 42 | division = CharacterStyle::class.java 43 | 44 | var next: Int 45 | var i = start 46 | while (i < end) { 47 | next = text.nextSpanTransition(i, end, division) 48 | 49 | x += drawUniformRun(canvas, text, i, next, 50 | x, top, y, bottom, fmi, paint, workPaint, 51 | needWidth || next != end) 52 | 53 | if (fmi != null) { 54 | if (fmi!!.ascent < minAscent) 55 | minAscent = fmi!!.ascent 56 | if (fmi!!.descent > maxDescent) 57 | maxDescent = fmi!!.descent 58 | 59 | if (fmi!!.top < minTop) 60 | minTop = fmi!!.top 61 | if (fmi!!.bottom > maxBottom) 62 | maxBottom = fmi!!.bottom 63 | } 64 | i = next 65 | } 66 | 67 | if (fmi != null) { 68 | if (start == end) { 69 | paint.getFontMetricsInt(fmi) 70 | } else { 71 | fmi!!.ascent = minAscent 72 | fmi!!.descent = maxDescent 73 | fmi!!.top = minTop 74 | fmi!!.bottom = maxBottom 75 | } 76 | } 77 | 78 | return x - ox 79 | } 80 | 81 | private fun drawUniformRun(canvas: Canvas?, 82 | text: Spanned, start: Int, end: Int, 83 | x: Float, top: Int, y: Float, bottom: Int, 84 | fmi: Paint.FontMetricsInt?, 85 | paint: TextPaint, 86 | workPaint: TextPaint, 87 | needWidth: Boolean): Float { 88 | 89 | var haveWidth = false 90 | var ret = 0f 91 | val spans = text.getSpans(start, end, CharacterStyle::class.java) 92 | 93 | var replacement: ReplacementSpan? = null 94 | 95 | // XXX: This shouldn't be modifying paint, only workPaint. 96 | // However, the members belonging to TextPaint should have default 97 | // values anyway. Better to ensure this in the Layout constructor. 98 | paint.bgColor = 0 99 | paint.baselineShift = 0 100 | workPaint.set(paint) 101 | 102 | if (spans.size > 0) { 103 | for (i in spans.indices) { 104 | val span = spans[i] 105 | 106 | if (span is ReplacementSpan) { 107 | replacement = span 108 | } else { 109 | span.updateDrawState(workPaint) 110 | } 111 | } 112 | } 113 | 114 | if (replacement == null) { 115 | val tmp: CharSequence 116 | val tmpstart: Int 117 | val tmpend: Int 118 | 119 | tmp = text 120 | tmpstart = start 121 | tmpend = end 122 | 123 | if (fmi != null) { 124 | workPaint.getFontMetricsInt(fmi) 125 | } 126 | 127 | if (canvas != null) { 128 | if (workPaint.bgColor != 0) { 129 | val c = workPaint.color 130 | val s = workPaint.style 131 | workPaint.color = workPaint.bgColor 132 | workPaint.style = Paint.Style.FILL 133 | 134 | if (!haveWidth) { 135 | ret = workPaint.measureText(tmp, tmpstart, tmpend) 136 | haveWidth = true 137 | } 138 | 139 | 140 | canvas.drawRect(x, top.toFloat(), x + ret, bottom.toFloat(), workPaint) 141 | 142 | workPaint.style = s 143 | workPaint.color = c 144 | } 145 | 146 | 147 | if (needWidth) { 148 | if (!haveWidth) { 149 | ret = workPaint.measureText(tmp, tmpstart, tmpend) 150 | haveWidth = true 151 | } 152 | } 153 | canvas.drawText(tmp, tmpstart, tmpend, 154 | x, (y + workPaint.baselineShift).toFloat(), workPaint) 155 | } else { 156 | if (needWidth && !haveWidth) { 157 | ret = workPaint.measureText(tmp, tmpstart, tmpend) 158 | haveWidth = true 159 | } 160 | } 161 | } else { 162 | ret = replacement.getSize(workPaint, text, start, end, fmi).toFloat() 163 | 164 | if (canvas != null) { 165 | 166 | replacement.draw(canvas, text, start, end, 167 | x, top, y.toInt(), bottom, workPaint) 168 | } 169 | } 170 | 171 | return ret 172 | } 173 | 174 | /** 175 | * Returns the width of a run of left-to-right text on a single line, 176 | * considering style information in the text (e.g. even when text is an 177 | * instance of [android.text.Spanned], this method correctly measures 178 | * the width of the text). 179 | * 180 | * @param paint the main [TextPaint] object; will not be modified 181 | * @param workPaint the [TextPaint] object available for modification; 182 | * will not necessarily be used 183 | * @param text the text to measure 184 | * @param start the index of the first character to start measuring 185 | * @param end 1 beyond the index of the last character to measure 186 | * @param fmi FontMetrics information; can be null 187 | * @return The width of the text 188 | */ 189 | fun measureText(paint: TextPaint, 190 | workPaint: TextPaint, 191 | text: Spannable, start: Int, end: Int, 192 | fmi: Paint.FontMetricsInt?): Float { 193 | return drawDirectionalRun(null, text, start, end, 194 | 0F, 0, 0F, 0, fmi, paint, workPaint, true) 195 | } 196 | } 197 | } -------------------------------------------------------------------------------- /app/src/main/java/com/livejournal/karino2/zipsourcecodereading/text/TextUtils.kt: -------------------------------------------------------------------------------- 1 | package com.livejournal.karino2.zipsourcecodereading.text 2 | 3 | import android.text.GetChars 4 | import android.text.Spannable 5 | import android.text.style.ReplacementSpan 6 | 7 | 8 | 9 | 10 | /** 11 | * Created by _ on 2017/10/03. 12 | */ 13 | class TextUtils { 14 | companion object { 15 | val lock = Object() 16 | var tempBuf :CharArray? = null 17 | 18 | fun obtain(len : Int) : CharArray { 19 | var buf :CharArray? = null 20 | synchronized(lock) { 21 | buf = tempBuf 22 | tempBuf = null 23 | } 24 | if((buf == null) || (buf!!.size < len)) { 25 | buf = CharArray(ArrayUtils.idealCharArraySize(len)) 26 | } 27 | return buf!! 28 | } 29 | 30 | fun recycle(temp: CharArray) { 31 | if (temp.size > 1000) 32 | return 33 | 34 | synchronized(lock) { 35 | tempBuf = temp 36 | } 37 | } 38 | 39 | fun getOffsetAfter(text: Spannable, offset: Int): Int { 40 | var offset = offset 41 | val len = text.length 42 | 43 | if (offset == len) 44 | return len 45 | if (offset == len - 1) 46 | return len 47 | 48 | val c = text[offset] 49 | 50 | if (c in '\uD800'..'\uDBFF') { 51 | val c1 = text[offset + 1] 52 | 53 | if (c1 in '\uDC00'..'\uDFFF') 54 | offset += 2 55 | else 56 | offset += 1 57 | } else { 58 | offset += 1 59 | } 60 | 61 | val spans = text.getSpans(offset, offset, 62 | ReplacementSpan::class.java) 63 | 64 | for (i in spans.indices) { 65 | val start = text.getSpanStart(spans[i]) 66 | val end = text.getSpanEnd(spans[i]) 67 | 68 | if (start < offset && end > offset) 69 | offset = end 70 | } 71 | 72 | return offset 73 | } 74 | 75 | fun indexOf(s: Spannable, ch: Char, start: Int, end: Int): Int { 76 | var start = start 77 | val c = s.javaClass 78 | 79 | val INDEX_INCREMENT = 500 80 | val temp = obtain(INDEX_INCREMENT) 81 | 82 | while (start < end) { 83 | var segend = start + INDEX_INCREMENT 84 | if (segend > end) 85 | segend = end 86 | 87 | (s as GetChars).getChars(start, segend, temp, 0) 88 | 89 | val count = segend - start 90 | for (i in 0 until count) { 91 | if (temp[i] == ch) { 92 | recycle(temp) 93 | return i + start 94 | } 95 | } 96 | 97 | start = segend 98 | } 99 | 100 | recycle(temp) 101 | return -1 102 | } 103 | 104 | 105 | } 106 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karino2/ZipSourceCodeReading/fee6b82247e233b0f816e30a4e68e6c9ed0b76d9/app/src/main/res/drawable/cancel.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/choose.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karino2/ZipSourceCodeReading/fee6b82247e233b0f816e30a4e68e6c9ed0b76d9/app/src/main/res/drawable/choose.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/filer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karino2/ZipSourceCodeReading/fee6b82247e233b0f816e30a4e68e6c9ed0b76d9/app/src/main/res/drawable/filer.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/gsearch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karino2/ZipSourceCodeReading/fee6b82247e233b0f816e30a4e68e6c9ed0b76d9/app/src/main/res/drawable/gsearch.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karino2/ZipSourceCodeReading/fee6b82247e233b0f816e30a4e68e6c9ed0b76d9/app/src/main/res/drawable/next.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karino2/ZipSourceCodeReading/fee6b82247e233b0f816e30a4e68e6c9ed0b76d9/app/src/main/res/drawable/prev.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/scrollbar_handle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karino2/ZipSourceCodeReading/fee6b82247e233b0f816e30a4e68e6c9ed0b76d9/app/src/main/res/drawable/scrollbar_handle.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/text_select_handle_left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karino2/ZipSourceCodeReading/fee6b82247e233b0f816e30a4e68e6c9ed0b76d9/app/src/main/res/drawable/text_select_handle_left.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/text_select_handle_right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karino2/ZipSourceCodeReading/fee6b82247e233b0f816e30a4e68e6c9ed0b76d9/app/src/main/res/drawable/text_select_handle_right.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_search.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 19 | 20 | 25 | 26 | 27 | 28 | 45 | 46 | 62 | 63 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_source_view.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 27 | 28 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_zip_choose.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 25 | 26 | 27 | 28 |