From 9065a9cb83f6dc01ca89bfabfe44404e15438682 Mon Sep 17 00:00:00 2001 From: Ekaterina Shliakhovetskaja Date: Mon, 12 Oct 2009 20:04:23 +0400 Subject: [PATCH] Spellchecker refactoring --- .../spellchecker/lib/patricia-trie-0.1.jar | Bin 0 -> 43800 bytes plugins/spellchecker/spellchecker.iml | 11 - plugins/spellchecker/src/META-INF/plugin.xml | 15 +- .../com/intellij/spellchecker/FileLoader.java | 56 + .../spellchecker/SpellCheckerManager.java | 405 ++- .../dictionary/AggregatedDictionary.java | 120 + .../spellchecker/dictionary/Dictionary.java | 86 +- .../spellchecker/dictionary/Loader.java | 24 + .../spellchecker/dictionary/Processor.java | 23 + .../dictionary/ProjectDictionary.java | 281 +- .../dictionary/UserDictionary.java | 199 +- .../spellchecker/engine/BaseSpellChecker.java | 115 + .../engine/JazzySpellChecker.java | 286 --- .../engine/LevenshteinDistance.java | 43 + .../intellij/spellchecker/engine/Metrics.java | 22 + ...llChecker.java => SpellCheckerEngine.java} | 99 +- .../engine/SpellCheckerFactory.java | 53 +- .../spellchecker/engine/Suggestion.java | 65 + .../spellchecker/engine/Transformation.java | 48 + .../{ => inspections}/CheckArea.java | 10 +- .../inspections/SpellCheckingInspection.java | 5 +- .../{ => inspections}/TextSplitter.java | 4 +- .../options/ProjectDictionaryState.java | 75 - .../options/SpellCheckerConfigurable.java | 222 +- .../options/SpellCheckerConfiguration.java | 45 - .../options/SpellCheckerOptions.form | 116 +- .../options/SpellCheckerOptions.java | 356 ++- .../quickfixes/AcceptWordAsCorrect.java | 3 +- .../spellchecker/quickfixes/ChangeTo.java | 20 +- .../DictionarySuggestionProvider.java | 3 +- .../spellchecker/quickfixes/RenameTo.java | 10 +- .../quickfixes/ShowSuggestions.java | 58 + .../quickfixes/SpellCheckerQuickFix.java | 10 +- .../state/AggregatedDictionaryState.java | 84 + .../CachedDictionaryState.java | 92 +- .../spellchecker/state/DictionaryState.java | 97 + .../ProjectDictionarySplitter.java | 10 +- .../state/ProjectDictionaryState.java | 118 + .../spellchecker/state/StateLoader.java | 56 + .../intellij/spellchecker/trie/Action.java | 23 + .../trie/CharSequenceKeyAnalyzer.java | 144 ++ .../spellchecker/trie/PatriciaTrie.java | 2254 +++++++++++++++++ .../com/intellij/spellchecker/trie/Trie.java | 172 ++ .../spellchecker/util/SpellCheckerBundle.java | 73 +- .../util/SpellCheckerBundle.properties | 55 +- .../intellij/spellchecker/util/Strings.java | 152 +- .../classNameWithMistakes/TestUpgade.java | 2 +- .../inspection/commentsWithMistakes/A.xml | 4 +- .../commentsWithMistakes/SPITest1.java | 8 +- .../inspection/commentsWithMistakes/test.html | 2 +- .../inspection/commentsWithMistakes/test.txt | 2 +- .../docCommentWithMistakes/SPITest6.java | 2 +- .../fieldNameWithMistakes/SPITest2.java | 6 +- .../testData/inspection/groovy/Test.groovy | 2 +- .../testData/inspection/js/test.js | 8 +- .../SPITest3.java | 4 +- .../methodNameWithMistakes/SPITest4.java | 4 +- .../testData/inspection/php/test.php | 2 +- .../stringWithMistakes/SPITest5.java | 2 +- .../inspection/xmlWithMistakes/test.jsp | 4 +- .../inspection/xmlWithMistakes/test.xml | 4 +- .../spellchecker/inspector/SplitterTest.java | 19 +- 62 files changed, 4700 insertions(+), 1593 deletions(-) create mode 100644 plugins/spellchecker/lib/patricia-trie-0.1.jar create mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/FileLoader.java create mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/dictionary/AggregatedDictionary.java create mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Loader.java create mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Processor.java create mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/engine/BaseSpellChecker.java delete mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/engine/JazzySpellChecker.java create mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/engine/LevenshteinDistance.java create mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/engine/Metrics.java rename plugins/spellchecker/src/com/intellij/spellchecker/engine/{SpellChecker.java => SpellCheckerEngine.java} (66%) create mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/engine/Suggestion.java create mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/engine/Transformation.java rename plugins/spellchecker/src/com/intellij/spellchecker/{ => inspections}/CheckArea.java (85%) rename plugins/spellchecker/src/com/intellij/spellchecker/{ => inspections}/TextSplitter.java (98%) delete mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/options/ProjectDictionaryState.java delete mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerConfiguration.java rename plugins/spellchecker/src/com/intellij/spellchecker/{ => quickfixes}/DictionarySuggestionProvider.java (96%) create mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/ShowSuggestions.java create mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/state/AggregatedDictionaryState.java rename plugins/spellchecker/src/com/intellij/spellchecker/{options => state}/CachedDictionaryState.java (62%) create mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/state/DictionaryState.java rename plugins/spellchecker/src/com/intellij/spellchecker/{options => state}/ProjectDictionarySplitter.java (86%) create mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/state/ProjectDictionaryState.java create mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/state/StateLoader.java create mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/trie/Action.java create mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/trie/CharSequenceKeyAnalyzer.java create mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/trie/PatriciaTrie.java create mode 100644 plugins/spellchecker/src/com/intellij/spellchecker/trie/Trie.java diff --git a/plugins/spellchecker/lib/patricia-trie-0.1.jar b/plugins/spellchecker/lib/patricia-trie-0.1.jar new file mode 100644 index 0000000000000000000000000000000000000000..5d1ea6b242f05ceee43f71b4b0d1bd12d9dd4b0f GIT binary patch literal 43800 zcmbrlb98UPwl0`t$L5Z0+ctl(Z5umIc5G{hJGO1xwr!i8ch7mZ``qsH`rYm|My*kQ z%r!=>s`|e9%{i;&rGJ1z1O1Q3tR{)`KRW+>K>>jR0YsGrX(eUF=s!n+faL#a3JGNT zZ3-E0vikem^v`!g{cith3J{c)6cbfarUQrpB5R;~84v}Y1U`4fa20p;e{9Uk;(~_k zQkn5Q5CDWd;6FP`63#qhTzmTwGzYYc%iHte;iDEti}-h$gV6;|R9XQDLGs78uTzUn zQ5~9QwvDXf5Zr&ANHCPlA2lEBqHoVeZ}W2(IMle6kQ2GsQKa+XCn+zSQ^<`Tbmd;TiDtD=STmT zGUPioV$mN!K)?_{KrH|MBSAwaXGa4gXJtnV6A~cTn7vOA;;?XPmp7zY%q=S8fT^uH7+rp0uR#jUb!two z9#is>TpB35oCC>-jf2Y#Bu_tB>kL9Jef41W;pxQH;l%{AQ03|wR0pUIZXon?OmsR_ zrC~|6C$b0e7b!Bc#z7=y-CKkbJ(>FBIi_Q+qdn?)K-5fgxrXN zMWPTT-dxYuK4kU{K&xvA50B@d6Kt7!y5RtoB2FDo2-Wl;t+cC{%w;PV(&~={Z*0mt z;KrT_4~<(`X7&Me?hnWxNu6+`A;FpHH)fM=<41rFy*^;=xoQYntjC={>W^0DD3ap9b@74(k+?Uni@GZY;ncB(H>oz^wf zXEWbg{B6>dK{ou?r`yjs+qWmUf;TzGb@B+bl8RMNt;&jIAykg>F@vhK)WC$#$Yw;9 zxgZ+!`PKJ#nQR=cPp0#{92A;?>q+|qju{p3Plk-I7Y4?NbH4~5jM zMgizAi`vy8ri}=R%f9OB*^zcsNZFCgVS&c)8FT**Fg_!wh0gXWG`1?#sU{^= z!kk%B*~SBz3zWKspT)S4dZiUwsHREv{9z!)dd`q~!X~k7i>ZlHO^Kv8qM8?joK}fk z(U2c!f^$umrYwdM5as_KEdPl`)eR>U71S@A+7sIZfW@4Uv_;M=w1r}5cJ8cV zL4m|E#DEYYqD#8=Y>LA0IwP>rPj^>0Nm5kKDg;p9w={GvcpnBQFG>ucc_&_7(_OEg>iV>;ux| znA;87IcW*81O1hl3f-z4_AvRlsD+OboCq<7gBYUj+8Ifgndt2Mt(Z)W$8UGEA<^S@ zCM-7FC)1@r$08ykkT+xPHG0#LH{-7xe{cV`v^XK`V>1JzknGbx%&ye@SqD0=H9oJ* zJ0K%HhAE!#1_}uQ76{=KY*H4SsrbMH6S?(u^ViB(?@dkMm?nM0s`&LBhZ)Q{YJc2X zh?N=rk2_o3gpEple9>ra2 z4kcg!t=Fc!RvERl0pm%S&#fA)yb^1rKWBv5`07H}7MVaiYy~kDo17;Yc#A>gQ}xb2c@;D9Kkq)Hwr_ znSF4rl?-T=?2Te}k}x~^V*qn4-lDNcgZxLov59_l2kOaqgK&f-3O3yVZ=R{$6|o`X zYOqGE?zq!D)h{<1!zB@I0&DDyYb7%?gw^WUOP6-C>K^V&w=(Ot`XMD_WK(jCZJ}Pr zgK8mb6-miRnm}B5jFc>6w{W5)f$L1A8s%!bPxE#}7;nlV7?7GNS!Yq%ub_r%EiTm4 z<|fzk)?B*%V>8lFrU&?3jUl=(@piNif7r?+cqtkv<}lwM;U>;M#^Fy4UygR>suN@J zwvrdwPMZJJO;8{`npaR2npZ#>awpn!bG`TKuN1BNAGTL#$J?4)3UTnW=g1|b4)VP& zVD$PzJqliO^o3I4pK{~~(U%!!Szy*~68)UzcI4Z_rgwE0f!70}6TVx>S;>RKwu1Ob*U8F)ExHGToTm02wbIeA14c z?u(Ub#kR*{cK)&;za`hnk^(wjK_l8ms2|BOdLja^`EE?qVGQRTZs@cdIbP8Xit&m6 zUoipcbNIk#-oUF~SDxLjA2%`T?`ohv;9u1Ce@PDMcn+Ab&B^5PLqGhxFOZk#jD5h{ z)chafTM#_8m&r~)=Gg~#?}{-mT0(x?x0wOQ|5jAwkIT}DBS}ffBA(TSZO;D*nt@AQ z)BN|&@(77W6oKp5o6dtbrzL_RXU*^q5}?u@g7fTyzA1PO^Z>wVm5v-(TMSpaxYz6P z$Wn(0mrWz!k~O~U7ycWL*npEt79sIb(E(?xD1_2=%!?PEl)#RSk>+>(6Bl104k-SfeUXZk#7vvRbZl4T%Jlxc zWo`HZ{#S;7PiQG7`j&hekU&6;|DNH6%?gNeb z9FEX?+=I#wkRFz5Mhy;>#N?c$cXAXRRGEBW@NXWBTOu8|uhWn{pVM2$STr+km%yh1?Oe;T*O>q7-P#u-qcYbz$nP~FIwaeg_n1DSQyQ|3! z71;Did?@tw)M1e%pK1-&Bq%SL5*V;yqo!fMYSb#Op(Jks%yGMlgC)%PWU6(NMsx&e zjUiLndHL}oO9@zp)Z{uFTV%b3`au8<6Aq=CW#9-i6YmCI!C6UqV4gs$ieWm4Sact* znXEy+p^M8zHRadN<08G3eUIrF8gzzFNjJ}#a@oLp#oBympS|2*O~4n*TC=>J7Rys! ziVrUV@_`!qd22lu+{8Q{yN^K z{=UrlEUoK3f9d-@bps~1FAb)*fIk_VF)Dh* zF}btg{bCj9*>SKEBFi;LfCPxok7yR)VW@`LA$%quFS63Ruqrm{oZGr=wzMP)=6Qgc zTE&lIOPW9XS#VVyU)ZCRMVKVP!=@g>e*?L@$VPGlEaHnIy|-uJOOh}29@6|wF7zo5 z85th-m!QnghU5uRdo`fG9)|O0DRD+IF|yKeHu$l!_z)eJFAD2O)krB$)aFnzl@bSS z|Btd9T3Q_R4keMKMkIy4HON<1Nw~7OzevglnODX;J^a4*Cf!Xa-itD^;H%OMVktYm zpglb~T(T?=^`Ke0YH?E{uhMf;^<`ONFrilWa0E3F8-5i6m9Hg z28shM92XFWS0bRrGNQKLb8o+N9H;t)1$(xH!@@Mp`a??y7AW_P1z# zf4LcQ8R^O?w8^F^-$m2jqOM%Tz@k;S8_LRTFmCwSMtZH`Cf+Skq9i9I9?An7|jXvGln9&$Q?mZa^HcY5oCN|ncN{5qScZd=AQ?*{YN1C5bKu$^GV z1P!EW1TMLT%Yl(#R`}f%LWFKZAb~)D997cdf^4Qq=^n< zDkp=GNJ1Hzge(*b*;q7op_;(qg}i1v(X@~pfW+*PL#Z06>lWVZ0RD@M-s&(tFZbAV zpIWfbgY^ylUw0RR<_hh@_paIi1_C1ff7@OE+*IAZEG;=6iv+Yr}zkrDku9T;D;*uvR#UV3hsU|UHAse_8*4TTm4O?TSMsm0*`>;h5&S=w<$#xLr zVDzXCNtd_jML6DE5pzD;LhL)&Y+-SQn)YRd?jW!bA@A*Da(gG1My{^veC~tSyV^Z{mF8k>gtg(Es~cleBdY8Wd6xFzYpya&DAbGoIO2YO}+tHd0E+GTW${EaJ4Wo16}IOr@$IB4*IjX=`*! z?XdD^GEjE=cCY{*Ytl68$d8$mbdd-q=cyy-TQ{4fsR{~MfjgQke;I{ohz3+J&;6=4 zR)sZHy;w`*1lR)qD0$(2P0nI=u3N3l>2`3e5DZ1miz@11NK5XNq>#!@MQfzh@5esq zNJ5r4czq4FRoJko%ABIJ8ZoqDmHNZ!Af#)d7C)fnT70pgzcg5riejh*fq5@-$59#* za@D+T@FX^BWYX7hDqo_?>f)K;YCUQ6#q72qSj-r+D1(aH9ti`j8mkgJSndCG2A&RoBbtFuAovscJFHQCnzt~udwT`PpgPCePSE0D7g7wda;Yzc=%KHAUF!0BU z@kZxF`G9S=`12q1+c1AQ2L*wx8Y#5)=yoBLuTp;;T?GdJ2xz)J*49;&m_&b)$(eV& z3%tyyueG6(qe-?J*NC$LRz5+^)w~*({MSKnW@>S=yoF++*FPf{xHdA*;ykCx7an6G z6Ee6EKclZJGh;WyV!0j1Tr%>WXyAd(2xhmz~yG%!gYql`d_4Syq0{g1K^d zZ^-8UKI4(5i*u%#3*^56-JPT(xj_=~#g@)+s6l`%iT&iF`;3nLf&$4i+~pp&)JHG* z0jh>TP;n_79h3%1o)+4Vs#y%I$vRUPv#HnLzCSfqiUZ*5|91YZUZLcw!rU+R+Rkc7 zDyyO*a92dfQ__~|QC>0GR9D`lFfpGmQQSW|JC5gm`Slw%bz|TLcis2MG=?eJ@H1{b zSnb2b;km-MWg>7;-tj~zElvyfD@!ULT)erDj69}%JAYhOiQSONPLU#tb8Oq?Z|5&Q zxHDcAN*26JR6_Q;IxOurCrWlwq(ro_6_$_MLuP2>3~$$REO^S^l^z;lyW7HM4w^?N z;W7*wh$D~kVKZ!ksx6TbJ+U^sCr}G7W8?De{0YGzGC2}2fqpng1@Q0Q8#M*(!JCnP zg8eJe43v9oTYpn?@wWh=`1eHfe+x3A;wNN57*T`lUa;i_lo4;(9l#tI9jF6AAtRtf z7=J_AO8A8j%rX`r?#yUE1M#7HK-e4p4w^Gwb-Fxp5sFY?oRua&i zK1o18^XL2wUHWH@3_)Ba{BdBK|7fTC7G`zjBv*}m<`e0H(EudqY}9Bk>@Ct|DRUrc z(|KL_I7)EFy=9nqwQ)=bO7mvg)~J1_H#QI+j_&miH<%A(w$b*)jR2y{am*%g7lJQPAh3P304lX- zgcR%pA5QZtq6-dkC7)1cppjb9`oajpzpj`~_I+5)_eyntH|l@CV)6#gjuu812LF+m zXZ&XlP)6aq#DO}NE_H;sjs*J9b`HB6f}e}8yS8+0P`nS0CME) z!wuwk8gd-eve)q{31-B@Y5wJ>haaZ_c5WQbo5E0=at~)kEu3x^r+68SUhYDAG*IvE zc1JY#5ao%8IRu^t*P~cgrEn_JqHjh+nWM^12z0{koR9!2YeAl!<=KDvFJx@wa*ywV z!xR({ko5oNf5|(Vm|D0g8rYhd{72bA$;6rDKRS}%fw6(J-9IDZ4pkc^6t!=a3bjH~ z$$Fb>Gw`I|3l67T(GXTl(5RIsFX**Iw-2tmH@HHR=yO?R|61@dJ9PougUSE=<28?q zFN6l8upVU*lez9G!}E#f=wtnS>#N-zMjw^Ga5|t$X?WKVtK(U$aezgtnPmMBO^Qhi z(bxh@io*_>$i61DI0NO{ng56CWG4#JTK5yUEftCGYwKwx)qUb$PHNelL$`>ak{2@% zLJF#U({<4)sATU7#Pp7y1d$F+n_~gN6=+j{XGBl@MO2%ha8EI3l>TU5xktB-kvrdY z1DkzBxT&|AoQK~RvqIyMg|B}v9AJLMaYK)W!^cGQZ3+hFK{KjxqgCzCFOV%AdtAS0 zZn(^iSwa5rHt{5k?sIJinOw-^b+KRb-HAe1_hR6-%-6~5k%6u?C?6}h*|{D29<~gH z*V1&g$&ivSuD~{)=CXu*3RTr)D7c(=_>{zON$wu34A%iduIiHkEjh^t2i+BDwp{de z>8xC>cj=8>BxRLqEuvA-(n#yn@S>-g3kV}3pa~aIM`!2{XsLx6#&veg!fQj5sKvPE zTTrK+y?e3%TP&`*X!;L3xWRBT*bE#^)O zbp?HorkW{Ty;TZ_uX#FPBkt_g9pQ)g*)`SYZ>vUh=j+jG79>9caQ&AL)-=J0-%~CR zAO!s9hF$`CfiGL_W4WV=UiebQW}C-%!4LLrM>2U*IOSn)_Nh%Ooy;FRQ-i9mZv zt`aW*-q>}Zu6Dc%hC|$NJd2c=>PNyqCB-q224{p-ghIJk8CkWvE%}pu`VRhIsjIm( zE~5F{(UgD#0m=RUfVh%{t(monvz_g~!aP$&M`=y~l~;O}E>U?bgecr!F(1yHqByid zr8{~KDXs<>IyE$ zW*yNk1cn6&jXLSjJv&cNEFZ6s>5Zg^17#*DNrCkXEX@?+UOQ$9dQs2!K!UJ0OWlcmfR7{2WeLPOzT555 zBgv7pYF%;RH%z%L}22G5>no*@boZi%5k)~_;enwn_f&7d4j0Hg{vr#nZM~ z5GVM!=E5+tGgN^g!_QxK#pr$$@OSZNMqWA=Lg;`obv&(zm3OnuBQ*Z=^INzMqELPHMhic{i`-+%loNRp8q zm^{DzAnEr>2j2hhAo+*K?@-l}Ls3Bdgs-rshpG2>>r$+zl@Jt|R+XocLPNC~cr&qx zacQ`W-v|)Y^S+08Huri0MnR+xt0McP*vr_=_8)0vWLsf2Gd+4c)HW@AyIWei0s@bH z%Eb-fbJmxb3_h@*4dKPOF+?reKd;Bxb(DA>{w3BL`N9xg=abPFh#nHFY`+=8CPUS{ zEsThcCPOK~SU;utUeoEU!+h&Ci?}$(f(~mr%FvudW8OGn6P&!8p17S%CKEyUXEDml z4`xoi$=$KN`z~!8nvS{NHam0XCq{4~fEs_Ud>zJ?H^O8>$j>au7OO@Bpzfi%#FRWK z+)&IBz+Q0#2cXf+AlwNgifh4{8Xp*XWm9-T4^OV2rpeI{ZZ7b3FoO+{Z6;?)4=E+c zja-T;EsvQ7?yML11XjG%&*|< zLpjVDp`#@OO%b)))a(HkLs_@jHnq-Q9+{^6Xt6@mbTO^Zk9HoKN0BZS5uQB;9L;v$ zzDhym5MsPuGPs*;St#Ife)_H?)W75<V? z6FX=Tc}-d6;{4v)&eJJj*lQ-}V0}KbY4*SIPE%AmBJeC51PBPy=~uMx=^`Ol^73&B zH$-chSL>jEUYzI%E6q1RtJS-rUB<+AAuA#5csOK;g`E+MNOU3 zTp>LNH?AS?=n3n;VEdDrNWZdL>upwNqSPRSDv%@?^B7r7h+@D3s)S%9KY(J$LN|(u zqfb9FX-JqOf}Dg0v)__u;JgTvr;qJ#(zP~XlACTXjG*!kDHOf5n`pLuzw%oHqhIYK zp^37E1l6lRUr&OtWv?YFB+eqZ^MF)(>F=htD302ht-p#D&7E>}iXcR?<@u!u>H(O^ z^LAK$HmA)3ekRM!BDmOuo|bwcGfUR_F4Em8vy<@?LRRw9K}H}87r~s<*?+wAO5IBd z4M-C5voSq{Qw&ozg_K$W0Me%*Kw71SCuo}G?W}r+GLP<>@Mn4y-|baZPQEE8m~zlP zWPyf~H0R43E3?VU41HCJ3pGFxP@#iJS;FBTUx-hH#H6KAr=IsnAv$&Gw&N<&1R2QE zkpz8cUTHtWGm3>=wp#YP&=ot{&7oHrP1E&`cpttuQ9knk8Vrv8~7f;R78KzF5`j(D_5ec?$2EiFV+;IZL8YX|>o= zEl!AbR&hfNehF#`K8pwP{Fn4n1xQOA7Fkm~lMt{?ZJD9kAi$8$4XO%CfS~5Sazvc5 zy(LbIvX}(fk_v4TD{qqSvX)+*?I_cI05Pj-aTt(V(-w7sr=Et{q&m60G5L$RKU!Tz z3^$jF2$sz-Au_XBd{EUo;Usmfw`|E|{%&vs7E7HjxHypAxP7Ew%RK42G1B$AGVsZ` zJq8{VUq2-f0c=#K6)-5?{#Mg{$P>R-lt0fvp8<|AUSZE z>^0vVPgx)C6s~z~avKD1(%=(pwC^1EUvI2*RuOl3X_g5P&6FGcYWIh`M_wLC$73fR zlyjFz2T7i3BLFUW zPkgf-zOqe`3}=eyvPK(1Egj1#dwuCmF{ZQL+rGz**$Xo|lBnLpxyR5{o)=7$`rfvb zyY(O2-!30{U>D456-KUnLWQPUz8n{KvV!VYKhLJjoXe*3R7k+F<9Jn&@z&KWb#ZM# zbY)4^^@P4~Wm$-nfSgd1ku4Y{Dl)uSu8WmMV|V8{sSi&iakLDZ3CGNl-5+gmE_N4dL~y1oov>A zRHJl^_(FC_+MHNL*nGcup{j5sF#-~*F_Qe0dj34`V=LX)=}FK(iK&Nhrr&9l4KyaU zewhuKC{C`nqqU8z^Od^qxZ~@F{I|h7h7kEC-0Zb3qz*=k1M}}NM3Q#=ev-o8<{$)+ zKc@X5Jmc4~2L@~+zd7*^-J5X-S-tu%FNf73bI>BnKmUY~;$7aZk~1X>hwdt2{vCN1 z640%ysKB&Q%O-8hxh%#MX1z;&@x<1g#6*ImJ9bxc32!9Z*2*9RbQrZA^ejyP&HseJPMgM=mv$0kqr50__iXFm|God`XRDqi)_GrWyV z9wZ1mGAmhme2EG7Dpe{<&Zi@C8KLY5^|82=AinP2N~wSy=O9a_u`cW2g_nagbLI#H zr%gJ7>h`Q^&36Cd0xoUgV6%r&GOklO-eb;)x>D>=E38frI2$F*=0s@Jzc#|>5q;LB9p`C@9AIX!@T{&oSZs!2qDK711#-nw9WZnLx6>4@8T7Zn~2@XL8!fOMwQE)4;5R9q(W9e z%A5_MZZDp~lVSYkLYJ&-M=|{CxD%TGAmt6Ww`y%-gRr1-jelH)nCo9 zH3|RL7W0+|f@y^cc6+s)O(AjAeZJ$BkVV)rDoz;8#Sf(^gN^;k{TA(4e3)Y82HOsi zNoCrP-wVqAOg_aIgP8z)G|?!w&>4C+^bYph6&|a%YiR73Bzu@@ADly`BezK{4ZGt`xd2c>@y^ED)q>JA4r0 zM!PLmhY2d({Ai)ijX;Rfz5J&cv5pm*VuH_&?q^uuQ8d;2BHqkNY>YIlM+>8~^Rsd_ zwi!tnr46ZRDaFmD3%%dWUodxs>*?^bFEn18pph|&7@egI$B1)kFDQe4#R%og2(E~W zgMlH_>j)4k=U&eapptlqwx245#*26-C}HaS+Q8pFkb1_he?O$$GmXd4oywWCJFh5Z zIN&+AX&2seSASMRCu2}_;dHE}F+gQ+C)kKBF@^dSDx2a_YE`Y1>sSWMA%<5ucTSbG z!s->Y^dY!51dGCOEE&>_Pb6%v`s$)sE#;Y;D>En3T6}Q>s_)8*rDXGz{8ZCii(#;t=`(zQ-7mm~ zVjic;lo+~h)lEI;;I^!Vh7M~4xLGyr)O7fq_5!rupFoG;U{c3J$qklYitEO)MJ}1x zN}LVy+hD;vEA4SnrDCQL*k?1&d@00=v{mBhDs$7bX|2}gJn{pCApNX5%qYls@@_i{ z9z)l3>OV5khVwPI{q_VJ1cazzA4|M~!|RPG$1J@vpxfG+l8XuN8I|)1DoTUgI{^>| zFu}U=Qe2qGWSBTfSdE~<2TF8lnsi)6w&0`K`o}z96O8-;U&DL%aB&O+K*9K5S3Bxb zG^$kDG+9Le+hWsAeK!l%RfQyQ*}c85ax+7jL6!Qk>pYb~VUnO1i9w_L(d%5vK{%c` z#waoJGgNSYRT#F0~fJD_KbPx%tg*id2FkB6aAUiT^-V&-s3Azx5 zniAi}DP3t-9vn7x-{B!M`0++VN~h5t-A1qW?u}9@7e~f`Nd!6)HsGK+*;BX*K-} zgK`xm^5cUI8(bu9`wVN-FKsEFXMA(?-iOiomNB6kCe67sXHq|7KO-BDYMJmPfz^5K2Z8V2t*uVITW%6>XTy1UdUo*sL)~sUZmm7&&qt2VvNBK zFV8XqBB(e^`8^Mi7PcZ5Y%7sdjHLK;RI?N2Lx2-@Vx7*=-Jhlfwrt_Bor$gG;hboC zjP>iY6aIe~bUa;Dn_<0XS%P?V=W)e3>dz}=T6-6D#!MlPL={k-)r$X+k2uMmm;`@X zjM0&}aCgz1zl~(5(1d4Lshc{Ax!Mq$9b>K`*3iBg1P_-hFt{`s!URY7WQa2<^`)L) zn}5+E4lv@y@~0b{qfwjOBEKXi;Wtr{U6Ah6Vac9@2&dqC`NKqE6z)Vb8GCxU<5*91 zXh6HIk1BPic<*#|7(pSiHP$(l7rA;!>MbUqEm4p))rw$S+|CX%EKlnjr$oyeW!?P5 z{J`|*?QqU7-H}Ngy(}f)B0J15X>B!7)WNwZF+ioP-Jb{|4fjz`3$#|KP3}m%wAa?L z*VdY9QP%$wo0x%#B930eIhvWU_Xr`=q5Nq^)0eL!Q@$^vk(I*ok(P&Y#BsSzH^kmt zwyPEGslGi-IAuTJ8sBIuXe~D*9S$On(8~GqfH(|>3O1S))^R<>OEmZq{B4(FBvwXy zw>nnshBt)v0v8?~E!yP0o56nj`S=+dk4|`(0PLiIwT(Wd&e;5CyHLc>K+lOrDJ6fZ z?O|2Yi%E_o&=q5vsoe|&Tab^*YTPE>ETob(l~O*$L1*#)#tjOPEEKUgp~^mOJLNxL z!0H3gKQMgmT$G*;ht%|zho1Ob`oS%E^XfNH*ETO9oafM8$Z1v0H5fZ`OyoG{ItqC_ zHB{l|)~skBbP1?bKU8iupRFJ@I@AfPmAcoIzDk6!+$Zyddpa16EHm52r4 zOMXDk;&YS@uy@x3KAQ~_I^lzCat}`E3K2q2 zc=+;*jMIwLf3Oq|Gh(>;_px|9PuU+sfj1BYRjSNz;rd4R!N%BYr;|$`;GIY=3^Fu5 zF5zo#81>!3ReCzZ;u}=UI;DxVcAF=tMnsx{&&K|tRv*4K6EUM)azSSTV`1_j@HjwC^S3NkI0rX@B z>l6E5S?lUA!K2LgwHlA_Yc)*&9gXmx@llf6g%XMx;umhqzK|mol|s#8Av8w3{=6!* zziRffqI|gs_o}5Qi5U-+>&Zq?xVNvb??^c3Z-GT|m&9LvEDOT{>)#Sz!&w)Wk@f=CVPRnV?-mUGhZsH?pP{#AOF; z;L%z}=W!#$R}R80loT~676HMTkb~EugUe0F1jc$!*mEIB9;elx3yIa;-L>+jk7x|! zW;wx)jb|)quMJ~rF0wg;!(tZhWW~lxuml~q5D4Nm+_nZzaB3}Sdzb$jtq99vJ>r>f ze%xjjfo}C2Z9F7%mXdg5>MTwT8lN9`feyF!55x19H-rlT3{7a)oMYUfsu0XLg*nRM zXh<_yGi8*km?4g6IFU{)YL6vXTdBC^HQ?rMwoNX|)4LyXd-hS7FtadYml$n62Cobr)a&&s>)wPkGu$<`99{JC=QSlx}(;Br>c^#S<7Vct1b1 zx8nPlqphqsn>O!)4N8s&1kot-UoL)t;g@oNYkoqKt7Hs9p$%#9Oq@c>&gxX|n`ZP< znGYDW$!FzRaQ-4LO5u>Q^he9HfL^AFOpG$rElQ43Xd8edw@un;c44)>6UB)rd&c-% z{j5A-_MBn#A~Zmer7#mlhZbV5K7eXI?c!EvLJQ%J#@rXvfgo3cp&vKkjwZJ)iS|J= zxeJCiN7LD_2HmN81J{`pYfJV)EsCn^9`&Ld9C*Rkl)c@ig5BUJ^|3tv5#cULTHOU@ zbsCy+*!(S4>J=r~?~pyt%DOz1%7Q0L$FQxdpT0xiM}wtOI#wTO9zD1KS56QvMQ2q# z47P^dUO;8qInUI>Kvei&zu73#?bj6XkT)4~i4z*51l4 z1MlBU7D25^s>|Zs7A!`HvYVpPV7IuB+l0a9Ay_LBh1kGI6PG0Eg;bELh^;~T+l@uk zO<=0Kolnb4eG;xbyxLcGf2xTmjI^+8=3nuC0WszHZ5m7ce%ut_-}M%urxV<0Eb*VH zZ!hp~B(?rfsxF{jR+VEHbIwfMr_#yD%E6;;4zO>%N1nCn;4u8 zxyu8AWCdv`E{u56K*l{nqGJ|MRMj?1!bP>LbM?sS$Q^ye+tjz8eD`a9>S7YSss9mj zuF%VzJoHyB=jz~GsQig*41LA;7Gz9gj3UXLl89p(q1mjjFrW|wji)vAanr=)iU{=u zeb{f!McWLL`yp2Q7F?TUg|||vA1(KY@sO);=p>;<@p89D`N}R7Vj4Yiq$&GcqyJF- zI>e+`{n`xw(y5>KVyq*ig_~{v(<10ioZt4;>Q!Lj^<4;iftSuhr!7jg8B%y;9+`xZ z-@L%m9g25duih8viU`SaAmUyQZnc-Cq;i|K<-qq}x%q^Fin0D%O8p=FUklYggcy z>v$1)u2oSFWMNRHEK~#n^05gV-Q`6G-nwDvl|>M*(qA{v$4%K&ZP+9DWqi7xaIaoB z8LvFI_+D;>V}R5NH9?MhhcLW&en&lv0Kqk{f!tM!tTdQk!K4`Qqz4Hdb!Z^by^ zA%GGP5*rZxrs5&k=>aOektcPKt<9dQjts%%>D``@TJ@y)J#?d?>l58`MC#=y`FDFj z5M0mrnl$-_WU6y~n;N_{N-UWN{N#7-bKpbgOl^-Sp4KYdyccwt@rsfuWY~Eg^90hd#vOM5! zlu|OV+qot%5$(`}NX?!r zAX_Z7nZ949czoYC2I*YTRAgR)#=*aoI{9G}j<;2MDs1CjA|iCbEGf1o<}%cpAHVF- zQd(tsiFEB;xLYb<(!>_4JYFw*x2X6>t;u$?*|LJv&_r^yl}@FR%Em%Tx<1b4=U%V| z1(mTLZo$OyA>JG4rw@Q##t^v}irq{SWq+X2sYzQ3Sq;jqZ=#=&Kw-W%@D5y=8JmE0 zvnEfN8P3n#jm(Z_F{7xByCB^%0}jy~I$@aTcpYIt(>Js8AyK|rPHHaXs`uCnCuT}BN%BJhQ0V7`G5VDJe) zAfN8&FR^x_fiRxNde!atx0JMg|F?Ie08s<2>#=|@QIg|NWX{=n6{%14I8=%ue|U01 z-7vVMjG9MAIrtRUSk{B9fOs8Ps zDL4Ece_YtBn!*{>9j?d$gQahpK^y_S6BnfRY#eD=L_^Z9 z^V3IxGU7L+lu)uLB!2RMwnHLKX@C1HD%v8psxocFORm_@hJCOa0h+`CA_ek@ya65! zoIb;t^qmb`CB_AwB~=T_QeMsWIFGQLdkOA0`T4QtaJltv)p3)!q3}K9nc=Yk!!VNM z+g}NhO^gdnJ4hVAIBfnz1t=?FQR*!W`tY)@n@?L~yJ(ymYS5q$jS-C_r-_OTdS2^d zUiH11cmd%jIt!$J~A|; zc$%ySd4otxUz77PN!@d?{wW~oh4xc2O|?|ID%L1W8<62^BCB^K~ zjN`KJ#+#^O?9@zx#CTSyy6dpUt;xAzlWGfXF~Vp0etdY^m=>zv2$SKVl03)K_PoxY z5$;+zD|y>M5HphIAj=g;5TfCLpipTKl;LuQq&a<}mr0)Um|)yx8Vg!BOyle;rzq3z z@yLmWGit}3CS}EyjFVcrJcU_#M5qRO(u1P*rrfu!N@-j)3r2kZ8;^<^eH04IeNfyc zbrcC2(9sesj{JXc_Ki`NHCUDrkzw1;ux;D6ZQHhO+qP}nW`-j&jGU;d?wPKyXKHoL zdOz-3@9#bLV4uDB0g~;O2z6W5G0P8t0*S0g(B9icj~&69lROk|$_R|C&pKIxT=y@OE7%tc}SezCG`TU_kw87xd{u<;hgdS>vwBpCf-G?`xNBCn_ z4cC}gAJie~vB_XrS?K&Kf(Oty>3rBJhTn{3bU1FW_-^YhL{i1Wf*Zl$_DXn@sAxz0 z#gkM{J(oX^Bh7!SFA-jvicrId&J{?MTjOFK!q&(NQMo(b%sVixugfbqHDQ2&$SN30 z90!k{E&SI`7(UwcEGaI@0g;0H5DL42iz471ID)QPD!XOUlBLvCBaL`Cr@Lkri9Bg1GS9;5X`SiFA3&OP)dIvg3Uh`uV; z*K8^uuWVB5NsGMU#xivksfu1>MTgaEL3q=Sd>MNySwCPmVAH19_4IQzGEnH(H{W_$ zR+Ve5@y#i>+pO6SuUn!wvl(Y7BZO+*pH7(YRdyu$1vm=rzL5g zl8`b6HyISo+3nZ;C#@TEg7K1>c%~Od5X_%%i?%gSF+`hYB=u-zEbCM}_cmi!IN-9` zHEgf61c)gDQ?kMD65O8gJQ7T+xjd_81W&PpduKZFh-$Z0Pa9dtu(dXdIPfYrLSGR? z;OlEuHWH;HHOf^8`-AXznT^0I`Q;93!U;!@ z)4xyyRQDt{e+x4?Ak3noaQK3f9x3)F<8!lH|3bRtEFzb9!ZBLsA|Mr_#shI*ZvHa| z#dRX-4%T1kpn%(Co8}pAHOwE>R;HReMx4nS#@qq7f5|j3_=C-SDPbDR5W#Z*?D)JS z#lGu%b!)aC|MW~S)+qfB$30r;EP*u~NK9p5Yvg@Xv{-hvf*I;Fm7jTn{7pk}OSP8y z$(6#+JRc}yeB>ys$-u^*d$Fy2@d6`ronwzv+jIslJL`zWJKLk(9S@Opkaxa+9l*29 zulpUz)BKBuyVCCO+$>+1S{EU@$G5tn7uH_-v2`91d!VdN{>JSNFT@Q|qEC^8S)((}t4i*GFg?w><`}_XmclTHo)JD^@E(=F{vw_;i^=^n z_^r+W|6X7G3mMpdZGFEzc#MhulkcBkZHdGWKl&$A%Q4k$qmhTHTB$O}y>0@5f0xWa zx(1#}yuKdXTYjaJc->=bVu#p=NVgK7Z~<;F6wy?1UndN4E&KIPO7^CS*8|7+Iw9go^3B#$>G}@pHdHV(7sDmdExexgJ=S1igf$B`#iG<;T)yXdNEUVX;je($onH5l9N@&Q- zAn#nO3<+LU#cnf<`>yDHdryl>XEp8* zWlc`*vQ^&CjvXLy71t{5=s2J;G$u#aBt{vI#|zMwuqnM3@f2xi6mV`}Jx5JRWn7uC zpF?ZOKUaJkJvW*7nKQ^vmZdkN0s|-a%d73ZFoVXHAlO2O321^t;vc zg6)d=GV2ub1kGq~k*FB!)kYzIF;T+YOLO%J-pAHO;=1Pn$5sVL8nv3$&WkU{oyP5q zRGVV;{>2-h$xF)~@S8v_^bJ)B{m)>kzsJMB5=Ue!ZQ3mI!E;}b^Z>KAp^+V|C@5sG z6~_oZ9FZX^aVa$cmCNr^)EBAP)mt^~sNAaJ117ue{dgf8s!HVhsXW%7;QD+yl|DIh z`StPm2?l_d4GF!~Xx3(%AFvE5RaM>%W}(J(3HnJV zA;^IBL><)x`sz~&nwv&uM(}P1tH>T3)+mW&hmuB^Da=h*XrD1BxH&xBPs+8BD1kdf z#@of?Ff6|*Gbf0NcpuC>uU~Fv7@G2NUQVV>y5BvN!5so>3VrXnOI-5!JA{-dvucG~_a#0KQexSU-&WXNH`Z*j=Y)Yola zl1(TpCHw7=V0SEOEAp4M5f(2jC^eI_>5MD0JJyd^a;v=QI>n`v-~3G`FvERFA;pyU zre?@A<3|ilVM<-3I{5t)`XoQ$yrS|QAtRJTu5&1@c^PH5!bpdaa%&54vfpn}eF@-KY^ zUlnP3_gqN^={dxhqPqNGeW1B zC@X7=umrsI$ddt%wPP;d3ke_(`Q3HZNvj$(n@7FYVs}b)?LJ_52IGqAwePTNk74vD z{QP(VI+%FW8gpxV3*lO^a9=IO?4`sI3$E=Z117Pyqa^J(rz}Bnx=FfsMs8yCiK2?0 z=rl{3e>pw-_u%Wm0_yJmNQNA(wCeMp0$0a`PS}QfkkDo=u$jo4m)QO<#~Fo7}0$&h{RSMu&bc8 zbiXtxUDc>9I}M#GL@A?tUVJ*ttgHKA>CX>cU(67?Dt&;!{$GfK+%_95;tru~2tw_} zekUw*#PuPVB`8bGTO&~c>M)LMrTbhoX6VD(Fld2@6Qb1qG9{J|Lz8QQMsC>eRvh86 z9%9|0CIUm|kiPQp>|mQmTw>HAcWL|K#mMA@9wc=l7oOYbSe<7w_!PwsMx++9U27+( z+FA8A#vg`+gumx4BWWa9u;da{AdCF+ot$OjSfnz96qX}%@y|WQy!dYQ*}@f@8e_#1 zWYTKIc$U_26>bEl%-_V(^Sc|_(1tS!B?FAgUw+(U6(M%MiHZ&1#PJI1n-O$ty@h`% z)CW&ll8w8Hk+>8_6=svYl1+r|1aRPpnB|*tEA4(A3oFHTfr^7Vm;4AtZDI+i;d2$T zU9&`GimeB?=bE6p80N0~R2P#kJ!BJ65REX=AYlzZSwQm`Yl~uZ0A+9ZnOV{s$Z(DS z3HHy?Pf|W#xA0wXQ@=NueE&xDf9oUvMilsd{Iet_S!wb+xgv8Pa($FRv(;gQ?fBd^J?_Zy|e znl{R*)XHn+oh_-Kijh%zBFLFCWn`Wo=ZfAH%O*|~ifI(;PHMN%B|@iLmPsJ-AiHHE zgMx(vsVIDnFA!TMT`&jP|ElMTzmqCie0l6NG|_{3#!oL%@SQ?ehZ?xs=^HTw1n;8O zZnE?8?Vn|Rs!>T_dXUmnLhcbvI2`P+^Te>t8uX6^t$DMvA09;oG7RA%wAyq#D2!D0 zM>}RQ9^#FmOr^rA?`ter|Jt=|#fI>&yPf|!AHn4pmQC`m(|&0|b3M%{@ksu4nbe`K z{%baHKzP$vpK6yD=76VE;=v2J1u~$%f83dEtzeyrTc8?fvI%;#Tih}_MR+}oV^>c& zyS6N=@D1byp)EwYCZi3NdImh}47!;`N28TNa1K_I8OKpuxTbPkb!h1C+QsX>*S}=T0$x9UlJ9J}_8l+(!X@#)HE92c zm;cC>O47F91=u^Y&G`TwyqJ2fM2_Fgx^yWh7K%JMue2;VVyOZJJFexL!iv77X-6c0 zcQ76bEDk@O@8H}00N!x4oFYEZa-5T~)7Io^I_mTEW_A}~!_6Q+#3VVz6V<9JO|4n4 zuYPYpGF1g~tf|?l-PT|`XqK*bO>{4U8-lFp4ISZKtAE@ z!Yt?Y48)D&AEn7TJl#kIrDr4BgEu7vAA3SGL*LFuaj~EAco0f1TQtG22V#yG2(#>_ zN67bOr#HCSbHg0hmK|M)H-|j#QXu(z6c1c<{KNo*`t}0C{?jfa-S#m94~0UMxMmi! zVZqTDT8;9f%SxTow@qfZ@`;(J9vLELnQmr>B&b$g0)r!nxSMUspgO&3dK8C4mWy4; zC9}6BcFGS;Xw=sh{A|*H-1x{41}BOzUVWh^e}}LCW^eG|wnj83kuaY%Gcp;^AgvTy zxYyv7yAL7r6?~I>ZD>Ro!Ox`Y)$6)ngICmLoO1S%Y~wdws^!|D8b{W=>nUXF`<=?Q zL5Rt2Kr^c*jZh6ZMQ?NteE^>))vymE=bmy{5t{xAQF4bN;(jUINYCD!j8Gmpwlj{v z=)PrM%M6T#Kz+<~eZVjd!cjZldxm(0UZhRXF2(XI`7fAw@uI=7@^_xf|Bk)?ABLN8i=QfA;gJuNCgqBIwf9Yh9)Q6gN0_%16b!i48RT++f5b|=3Nh;H2&(8G_$9}ek=Qlfs|gqVi^ zCcYt6(7IQ`W*t?Ksr2kAThNh8*3&Nz;*vrVg`PGxwizxI6FORa8phboLDREK&B@v~ z5~uq0Tj0kpg^Y}BD}O?mU(FdB;@fcySVrf0beOqwJAU-WD3-tq#|-MxRz3D_fZhtZ zOd6zF(oWVj)11FoG1S&U%dKb2%oUkg&P$~IIm@ITS>%6Q0>Nku@>KEub4Ae*0@t1S z9^XX1*=PUGoBEq>`|oZ0ed-M`s4j`p&y6Se9~+6%*LFbUw?7ZM{cQJ6v+w@hsc|HA>sT1_ye`;ZHGO(9!+mfv3Roo(Ua?W z(@y%nxy$|~Q# z0~Lk^GzcX<`XD`8oA%Taq5bK6)w%oU?Ewx@z4;?SQ5CMdh6vs^y~e@IKfl4Uyx_1i z7Wj9kLkl z9hPhnpY{%MPGr!Z#Ztk3P}3aQ6zQ&KA9Pv{t3Az`-hCKOKQfRb*P&TFs!lw&+GOdb z;H5D>=)XwFf99fXCoDXKgoKd0TFNga^9KV$gI$&J|F)wc&dDHi7VJZp0yPu@v2R>f zfNgeS;ASHT6kK!%)xfnS^NrvCY+N3ey>=Wqy&G95J}oA;M)RULdjh*`_%R&l*wB3hZNZN9cl$CQY`A4oyBto($_%zslrlV~AIA4wy9n{r8YrVyqz5leG8&Sc$iuBLxJ zzF)Wk&=+yv5*!TU7vLh^K-rH-cBpp9o9Gti%xAgC`|NzPtm_|aIR;F>uMh1+SfRY>EW4l(CRE^40g6<1Gtk2NfD^;?9u8 z;@^hRaCB!vwPDPThv)7*!~mD#q>M_qy359r#kO8rKUlvd5$J0=W+yeV;AS_24mz6#Qn2dKh5p-b zCLeIerYGt%T7?5ywiD}AF*;@B+#eU17%bdpNy6h7@x0hV^jr@Zs~E0zvNsPsaFgp| zINqU?LnILj4F|}`(k)M#M;Na1_K(d#OS}Y36R@-G(03@g=e<$u2GlkP4eh3wtnf?= z1I(Qpcox4-SeE0LEYhH(Jf6HiSVyX%XPku5oLFDv$hZn_u}aKdem(`rJxRBxs0R>) zCF+i0q2?$Uz44Ul$?|bB0BE``O2zVVxTIO0Z6X)&S2c5BPZV!u#q?AXFMknNUSw(E{B3H> z{_esB{*4bvMc>NV*pcY(kAMD`%8C&1S8$Gd6ZI_H_Pvh2lfQ0V7$l ze&cH1-%I}iHQRg@90+5T;bvYiE~i{7zjB$eM&Lbk=onAm1}08;gny!5qJK)4oESHm z2u08va>x`jtR`%Z=yM_=au^0@OtkYL#bgTTfn0${9KT`ec8Eg!{xjqrpBsS)(Y_oV zGso9$gw_rH!3fcZgQpNYX^?w9TSg?pk0D^ z5TCZ2_hYS)<9YMX!~4d9qx@h36jmgS%f9(pCwQIdL31RF3i{hH3-h;Il$MvDbP%-d zgNRq#u1mcD`p4Gjd+AS3p$X55B`D8!TQ3ja7Mm)#@C4POi{$pNnaS*?*Hn@7&R}uq z6``{8SGvoSBE!U!)q*>d$Bt{{1qnK*L%GFOh-xGTv6X-oICTCCPKj1B-#{PqlzM^6 zjavRN5)c);zzLX&RmE6k4;ZGg{E*Ryunc)Zbr_9Sy+*N-Ek_udaR8LJUMoX?s#viC z8~Nx>Hw94T3%$rotRT(0zshl&jEzVGzf*49H<0@8%x(XOnE#yHEdP#i&8VzYBWJZu zL*YBjTW{y>fzW2+OYz3xXNZ~n4jj*CDpb>0L^b#z{Q&B{Pve9+pIDLYI@Yx=QErYujTlTCQz93$wxV=t4-vg4Kw&=}q33nqW*+)~qhH zx3-&l&^iw`TQ7oBtTAZgDG)ScB4WMeE9G?Ja4ky&u&hrm%eU9g57c zP5eSGB#aG+U>plyW)tMD5jkxt{bJMVw-*JikI_>S#xbOz(bpPg&q}pd9R@;G-Wp7Z zGDd;fQl%#lGL<2jpu6v#7*nsu8oniT`SbHJ5m%6&B&#Q>CRu4Byt?ciIal z$c4~RTn7E9BZk|C$6|?hD46U~o@FarRg0}>ZLqU@OX`$e%ooDJhK15fkP-u8pYZ-( zJpvM$*i5BqLX@aUkZ4y>ODfH8$B7PcIOrAfJbeUq{+VV-E05D(-$V2({KbI@%1Yzi zPYA0#FzkwOgiwe_VG3NR7^WSH;Np=)+_ja-UWh>!8F4yeMdU>Pkr6q>J8GdISKqqh z9^x0z8##7f3SQUe<4=qVqu`I@FvBpm1g6ra2?90Ip9m<fA57hGRXb(dseOEnPbY#f?*OVENeR~yB?4JXF>e6gnT&!gA|j$7 z08R;cn3Zn!^+wubu}CvB;_3aKcgA<-b@qbywD-@`5EdLa5P);lH0buNFd%LVKQ7ld z$=NO|L$v%g0U%a=f06}$kCG+Jw;8}*M(%-)S-0@yVu_1?nziheWZ!!dQdZ#)^4$kZ zbdO+RsrhehEHAj6{Q2kdS3qXoq7!usAELfkke6^6t=lXVABoS|GWQ)w-qrzHvlmGV zow|NR=@PW^SCslc%+WqS`)sY{e$dVzxiYWWdKf`d@wVJ5Md z0%}xC*9&x+Nm8$_>YK1?QWA?6lSZ%>DA6AVYJ{31s6zzpYfQ&v_0QHhAl zmRE-yDl(%(4QHrx0vWUyALj3LatlQ^)U#GJkhfOjYx9aYd?@BtP*U(928C6QnCQ1l zMOHozYoM$P2jmRpm$wv>H{>TCanESos{4Jd?YyaLpcf%RIA(L)BGy#Pf)0a|RPb)$ z`E$Q+2}=;P`4rSpzFie|!G)S%xkBP<(+}$=?mbD53U@df%VZ}1TX8!$QxigRWVBvd zX}0TPf2&u=f}1$l4Bl7V-~^6f9}(E$e1f_1tqmm{8K{Daev45dH~(mDQ^Cdu;d{+VeWb62_me=_tkgHHXzNC+1qg zRa{XDe#Pf3bGmN_>sbs$k;)B2O4b`I;I57eWzxLbWfhij6&G*QNv+;~Y>6yy371$cHug%`{Gmzf~0MXcXhoP zJbT4Lxy7vc%+Sgr{LCI7@{>rCnw*MKl?L=gBQX$i$B=fmeYqMvhRd4)FKeJsZ}^m~ zrdVfyl2mLbYnspC9y1@cw#AEJS=hL#m?4LYUB;v zv_z(Qu#~*SVc*vmT_CCB{F|>}LZ-EDPPFwO-lE2bGV54uuhzDB)yA^ov z2H{RWh15tI99UG19@f0#+2b>)^X3JNMo{Gy`KJvIK55zl_{mj7RBh$t3F%jD#*6VS zT2DhM7WW+G! z1QznI?$(~TsbADF6<`WxGW$0CRpj0@4jkBsgvX7@61^SeU?mAusSXBc*FBPkC%u+l z?hbd3Z5`;x9_-Dc$1K(%3smDdMX`E==`amWrbPSTO|J)r~IJ)`2R~_C;368Xl{}^{|9m5NeF-yaal+BQS4nVucJ9%shFyMe@Nx zC8?~IrD~U1Rmo&l?96jX2rWg7^}u;32Tu2O5MTjHK2R2wDPOyUHJZcfV#6#;B&)a+ z!t?lyK$SMIGRHK?ov!^mh1()CD#y!{L&o@Ez5DGPn`mX=&_Es8oE?Ma0O-esXU%D9 zMllFChbSj4o7i>v6MBWOjil`%Q0l3Hp=+RO3dD?V?=-bdfnWTs5YD(?z z?=IXsLMjaFEJ)n-4{#Q_t5vfBv@;fJZa3HuQ^}!lD^BP{1j{)mEmgjFIw(?2I@L5> zC+U4EFw$NF3GN0gS-RQN2fEzS3LSQJE}`BF6~uleDh(rgXZsJ5mgQINR$u^<^I z-%|{{wgRd2vl!%LX$E|LCLkWnMx?7u1Dr!t>m&N4h!%~%M9O6;QfV3@zzpS0Wq(8F z4G{tlt!q{{3V=1#-IC`n(x!2ic{7 zA+ntX_&cW0(uRzd2^5{Y+sCx?GT)580uFXW@OEYHy-;-I==Gwn-VOxb4zfM?frx@X zx{dm|j}X*`)_2j5;ejNAVKFkNy!(AI#iTmCO**|jisj1wS#Xi?*gceEf)$W0cZPBd{QM2Sq)}P}-_p`zsy{WfS0fn0&c&A9Fg%3N~&2vk%yILBp}FxeJ2KvVsjLxkwFY0<7;uyl;yD-i#5*V?OUe zgli`Pc&e7wJ-ftEE)C!mfXc-LkxlW8V<&+5#Npvb9sK;bw~lT*#;2<79UJwSojmJ& z1A;&~I$Je-P>UPD31>T2Oo2JHX3Sthj=kMuR6j;>GK5-0hH~;Z)Qty|vY)q=<#!BW z7t8->FX-0=rrFi^i~gA@ZB7Y*)GX3a@203{=ZF`y0L`~GFgv6u+YLQRAiPk?}$ zM#AqDoDJOU`HkKnBrnW^lONj*F{+ZNaI^2%^DoD2EU+Y^WXeOU*Mt$Y`LH{9-R(2%E@{6x+=SG`@Exd(4__|^W$jvUFl(njV`fxgXsPCyo>BwKJn?;$>? zA1!XhE8!XOK~aqvVIMed_fu9uGFs}1qop9=a1lS{PBOufD;Js4ufXI}u=WKxHk;jM zq}M#%*=q1YGjLfzo3nAZ!Q6L*zv$KYe-Y9Yt1Bj8WKaxO z0|jFRCT|l_nwsi8$$#u`>zcYSOPKWY%xrstCAqh|1j{hOsvN^*hH(36P_WuRs3vk= z0^g=)W}%-`g68?_!Tz*j`@sNkjFB|>ItW_NGc(+wj&>ouR1ffO>RkWET>mCl1*Y$e zy9D(-h*=uv*UapS5c}Z90$_7A{Ni|M3(`NTuPuANZYbxwWj@bzP|5@d^YS2HQQgWx zEkj8Y&yLl}sPt3{b={Pw50=NSEewk@1BB)0u70+t{0BIK2SC(mnO5*LL6i_T#qSZh z)zd3mr?;eK{E!r%+*X1du5LZghxkhi85j6D7M|bmIW8sJZfj5Wm-R|Y)=7MRVr$Xx zCTaXSwOwV759=>e0~8Y#$FDjUhK8JQ_7Fybx%jzdBQ+)7;!0PNdqVka^eX9C!)c6A z7;aqXz39R@A-Y2RT#+Sjb@5E*1yq?0j)*V5wD{kQVHJJW>SCVrfoq%&NK5)8+w*?!eze zAa^oLi=o0``%9_lH{ydYouuU04;Cy&k`0ip#F2KVB@n6$`-P0$6VKE#62=ea32$wz z_0)GT2laf=H}c5opkyfRK>G!`kR;2TJa%@wukEO6fb!J_3Tb9w?yt@7OmT)lM|1qc z8&6(mhg?vP`GG@2(vjt@hlr1`0Z4;jdtAS(WDm6k24wxP(?Nkf^5?rR1b!%_CGEG@ z8L(yX3WV1TMbg&~q9ZZ&z+JCS+bjz{Gt*|A0!xUqa6nqRe>@-^9-a>HfTs{}2(vk5 zv`K+@F(h!1%bu??G9J=_@#bW$*&|c9+ZH)Rxw)M_B>Kr9NzYlu@RNxe!jY^aBVC{^ z;G1_vvcD6L6WqL#7$7nww2Z8~CE!~-20EX`;RVWXmqb2JdXjHAlCDk_Zlk+mm?N&m z6o1H7?mDI<_;PxEo8jEshsySb>vP;jzqG()d+|(Dt9Vc?q_@?Rn$p~7?XfGAQa;(G zqC2*We8PV<&9iCweX{fYv95vY(+`HK_od8vXSv)BL8o5p#d(KW>kYhF1VP)sW?lp9 z2Q}@-!?z&!Q{6{dshfYr`{xZ8$Q_JI$v5)0^u6Jt`G1d{ir;?3Hm3jGNLpFfc2NcS zPka60kUb)+9l3t>Nj+~CXJz763Fv6K6Rkfy8fP;Y;4KX14eN1W26c#EM0?kDzM&##Ybc2-}W*GEj(k)W$pTgN{uLe<9|U8o6(;lp#TT_eIit|N`xox1kE3IYy3Hv3x( z?)N(R_j+PtS{hlzpGk-8`%K<3G4dP^?aHS_87_8_LuTI~DuJr=z#ioDsn*Vs4!XX0 zb?264kR^(3$`s<-*@yId3-|tp&QBSAg+7I*&V%Jw9Ef6`IEmt1nvFByBlmd4zWg4n z(G=pcW);WnC-T#u-gKTuN5HCHJ-_5c3~AHi!5^WY93DyPKdD$&AgsbE6A-wJ~9 zRz?Y3u+Ix`Vx^>)w_d^)BV`5~2V3+m1adz;n-*(CGCXq9&&6?S4`Fat zc5Tw$$t~M?v(iHXO^bQYxf)P;IS8MKW?bRDDU(8PVs~NXI27dyg`0<(G_c;!q46`Vl2fkG; zbT%hjN`^(^2nDN|(N3h0dBz2T(UE;J?Cs3r(XkW`pr#fG_IW`C!=bU>_&%Nl-|-X< zEA$KAJ$OYzb@Xtd5Mpr~{CDvC=zx3p@m+{h4RS!oY9>j98SQ$e6jU*fNxEp`mV6D5 zfeDf-t$~R~u0`LFNd>B21B@Ud`Z4H|ZP5(A%(0}jdH#tB)Z@{)HT+4Tlr56q1yi8G zw4MIoZsg5p-fAmasIlH~O11@y*P8>?7y$J%;jpY*gWC0qw92rbz`_N*rUtK2aF=hF91{0&wK9DPMd(Zbf8?XMJY` zv4>7kO!*5xE_b7iASGSs)&z6&es!J^OvsX?htK06d*s=7VFjvf37>;6_4nW&Auq26 zBZq_Z3$g@6iPn8;b10;3nLp-_!3BE)yBre0egx!$(VHsHTXjAE@=?|sAY zch)2Le`mbE?dSdhu4N}_O9JsDhs>sRWN2h~Tk2Be2UBf3#UBJCkYR#E%pDgcR#A7- zcS?LU)4TzGmYrQf4q7xyXhUB*?tK477+)Sg?;rqF<>dC%{ERyxhy3Em|c7H83qzUIKS;d|ORd6te{2F`!U!*KnH?y+r@Q~ewv@s6mYbu0+v4~~= z8As`&c|Sw}xvf)tf;{!sivGm?Yg;aa97S&YEK_NNUC%Cz1U1@C^W|7D5;4FVm59#p9^Z&| zl@0T2ae4DPZOi0`Q-v4{m5^*8RJi`)Ed34JgmUb0@zla<)s`;Z5z|2Jn*BvFnhgXo$q(eQSqOu{wzYP%Gys2q9Oe%wH8@0Y0bue7 zWIJ|H`+kTc7&zOGSMh%GBWf|I9g!V>?S4<18z>&TS2;US_Rc)hZjPMwRRpq`uCky# z=z;ySzc|q6$u0LKe!te7@5KPozkaQb|A%6){Y|k)4%tn#;uP=5U{-n8@2%QQI~Y!| z6xK+f2A2NOk|aPo;quL7gyl453Fj?eRfb16hr1OGH&rA+TTn=2cg)$J`nCYd+V%C> z0Wdpo2EolqB9NnOd}bOI_BUncLQ_LjBWC(6q_seX>zFULwc3rfVu*ig#KWpK=29Se z)HKY*xM(%5VlWZduM#Bdj%=1#KI6_`TjM5RqfQhPV3socc9weTbYP+j?}PGpM$24t zkQW&qmbB`01Iex9h$4~CD{R8U&M36!Vv|!K&2Jv*SnFre4|Ih5I!2yyfQ+e5NZ%#$ z+jCh^+DM6MkTLr; z>`siEb9=$iw<=f=leL^4uHCW7y1{Fi9bBBD*Z-hY7NxK7@EuHIz6AS*BxhTo?dx- zKS1|zmrx#*OshATc zuFcsK)9OEDSaTe?%-=0t%1}ofEb%n0Z+oM7y?)nNaFEmDH zrP?Oaoa9|;_MGZd&j?s_6YR&JA{!$t-LpPId3!@cp}M;Tg4-P?2D#by1_6mdVRs4E z$G{_dfv1F4MX9>4n@>R<20<3|$k zn?pD-6O0k{c<_j$aT|L77W1qD2YR+ zj?A|mMMIjpg=FNqoY^yPmy!zhnd#bCO*h~*B>{Cgk>ITu_b)#qKR0)%svZ9g`)HK* z%aDOl>q=WDYtntKHVaX{+GRrh_b^fVAld_P_|IXfxeI`|QQ=92iEKxm`(mlc$DSlo zG2A=ngbM$cI0GKQ?mj@E0UW?Z(JnR`{G-%+2qlZcA*MjvTbzPFK>s}IklQ1mi+xuJ z%kK*DFKlA|douaozE;@@+>$@|;X`)CrDPuT4GQjlKtVw<`zu~bi8+>pfun|>uWli- z;(FG(LYUp`-twwJpaxW*<<9*fka5@pLx7-xez<3NxTI>7y?KTMc&f_~zy^VeM3q2I zfq3P^B<_k~^1pvHe~9A~4XQf4z(?k|1h>UUVy!(ty+|Ep6=*1Fg1iX2U$NJP4!(le zZj_yQYrs*6(Q7@59&1*<=Dr!AglS)PNs-&ePjjVfo!TJcCNNZ*cfadx}deTJ?qkGodnI|Ybu)Rn^{RbI5vqN z+!Tx=U~ECdisXPa^Ti8#i1UdaQTK%gf)#f6%N+^ZQVf55{eTx?kctwMJH$C8n{aiQ zU6b>80_P01FcBwp#!<)u(0wMDz9|g5b(D>k;PEfID1UBrtg7!-{_FSl^Iy1Y|6ksV zt%H-X(SI@V(Nz6D6!R6rM-r4jLG9-tF9bt`9;s0+eiyJt7@!h|y14r(E4S#(Q@lsR(!lELR1fCLGrOEJ{`mZNAs|WvMISL>s`|) zESby*u5MneQ7WuQ+$=YZuYS{^Ry+m|^qI12UK1ybB@gVGv}<1E2b2i9kuM^BGLrPl z>Y2(?lFYv=4^M%?(SVs^FguJVJGdz=)r)Q14*TCcXB!Mz$cal_(gGesY zhl*b_B{pOr!KukI65_3e`7%3mnw6>Pt=*Vm1BtM%n=8ZWm6J%Z4bC;%s986f) z1$z3EDk`7C{QA#MwkHnYrV#t(4gLEbnRt8?4iSI0}9U0~Q9O7_2Dj zlQRpMNpTmQwY0NTuK{Iv^)MwP-(~xC&_BWBM_+Up#8@;Klc)F%7Vnf7#0s_ULQuxJ zJayz}q;TiMt6B1F_|iDf146>%(@-#+Qt9PMIfGJQtLr8OOwyp{E+-t6V67+lsUpTJ zIH`kHUv>==s^3Lb>^JASEAvY?87n(JLDe**RZojnnl~Yg=XP~cQWbMGbu^WX6~*u+ zfh@+cI0dXylh(I5D0hoYCp|l4gc~qwmH$Yd?eq1zI>ru%EiTb8oTPlp5DaC3tTKXl z8CpzQ?o}3Cs?LPMBWh#J5ZT6ktei7XsYw`DiZU<^)`85JXsDN1U(xu5Gpx@hW1yW> zE!-*?I8j|yHE$kdWw^nj%&kbF=b93RULKy9c|Mbpj^#d4s_qClt} zCucCCgi`72AuUUr5hA0kZl_@)Ivy2-q95B-uJz;qRnhm8sSyw^ZFZxlH9qWk;K97*}(dDaM+O>!7i{b$Nd;Y^zo%1oDx3+ouR*C z_O^z5&EW9}h0IS@NDbRx3^mZg<)B`h;Nw#Wiw=W?I3D9<@>lpbP6@aLKCyW_!t?>L z%x?W%3PZO%>sB!y@@V17#C;?^6qJ9>eCf}dT2@5TAn z%`0)S9u++0rG9wMpGLVa$qR6+8ZU5GN_#}{&Dth9DK3jE(t3&&f-7wF@q?L9qBc1JpmqW zZ0X>#K4xA<;S0~x=pJzK2zGwe`pMMs3T0+vq5ZYg!OqM39nHEegz}Y z*`uBBQ9*;;9o9mnbkh=?00lmXD-a~e9R#*^7`Yoxa&Jb6E3Nb4#m^?%Wwep)Mw2Pqz${>K0BO z<_7=cFEslogRc?VZ^5B~CqfC6nba$H%< z?P}#1m7ze?qUDy2x*=k8M5zXFi_O){ooDaeRBP)>g`&||@7mAYH;%+0<5(m9UB?E_ z-C3_5#>DhjpLO4_&M#6w>zAJa)NbD;gp2&3+~G1R4_H<;?*3c;qH=)Bh36lJ_OcjB zrDJf%5UHxxHF3a0AHmvo00epixZKj^y*=m5gZ7hF#N0FvO0A}EMK z6*g&GFQ6!1>n?H7$d(J)AO#sJ2o*W9ZHM`(z2b!F|GXsWib_6{+p6bwD4 zzpo~j&;p!PCun+!2er3xVnZviddi13*y@XdLZF!y7wWXoO!AQ29|~_H$*YoVm6nKG zs=kO=&mT`IhClL)#p55p5z9=`KXDV_VPYRT;)m;0-(s1Uh*Nn!ey!i)3uthq5sSmq z56+Cj4Nj0abc4+b1rE0NmQDB4C*YlAwv?gI*8jcXHIK*vdx zUT2X4be^|z6Ez98y8uebs!l7@(m&xDE;*R!Pl!lqJ-I*>#&JZKtt>h2@y{r;?U^jF zX&BZQJ^?DlLbeqNsnq=TgVANlvn4Zf-w`*?6X|z(+fm%$a)b~n|Fs)Y(}gz$19Db z#mM)HuZ3$9ngCnTVk++zUIZh)~BA=K9qV>Z4f)%m&)jz39v@u zLWJ523D8D*&XMk;lR0am-hiWBEpSB#z1wB6<|fOt}u<{Obr1!!SlJoO-`r$V&Qnzd@BP#ez#>}`2!vy8c7 zae4Ujb!&a>Z%Ys|yeh10(9*t}F(GOi*SwrJbwMmK!BB<4BDvCyOVWwfhPj;P@~n<( zw`%DYsY`PFmQT&_oX2p8Y-E=@%XqqFYO`lRk~4QF{>vTNZn$Eo?gN#yh5VR4ZC|gW zyN=crCKs(o^v`yj!hO7+o9K6=>hUB&-P7Vv{4k!E0-UGKRle*oagl+O-OXDR>VZ&K>f<=z^DXtFXIHr{4c#NmkY_!Z-aD7m6fS0rw zDL@O~;hl5}yK5+;8k*TbXle<=^9ZOFkjJiVc6cEN9i+uKOd_mFs1=CCuI(-#@x<;_Y<+-lsyvm$uT8& zT&>4^fapGPh^^$NZwEuI^^$DT5UN2BCCpM#qS4s0;MBEJ*YkEs|gQ>kYf$-Mj&eq}HnX3Hbe6chnnr~yRERv)Q zS|$p51`6xmuhLwaVsNtAyx&0gAjZdk!*GS=ZHrowu49}_ z1iB;YN)fk&LrOW$;q9oK5iEOq*~itHk6iW1fXPLG&v2+1`rOq4W`(@KEJX0Xo?&;DX!w3g-flb}|BJ`rhNdyqzHYj?#cEJ_-ejEU@BmZUNl+N5;>(=xkjtCy((*v(;<@8zcQ;!Bh!4ezR@Lbd(vTAdaVbgcd}QdcOBmGcfZ+blh~r9E3- zr$%#K_p|!spat<)1lAFfK4bD(=I~Bl7g<1}-d}b52Q7rQlgqGUhMb7FuymoRM z$}_EWvwHGmActMc9X)%cH$Q=|C;E++lNxPeO90Z%M3HWusM%P-e$Z5&q#W1(@6vJ+eeEa(hGhnKhHgq9zt~>$*jfxR8^~xgNpMyZ883a^Kc+SF$Q~uS8pj z2`UC6$HHLt(Me%KSCp`Qw3_CLyv?ptxLw0ue{7$m!V@m2FwfBX#$yx$zj{ub!7E_I z{l=Y}XlV$^ml6E3nB4&JQGQ7*f%6Oo+rEuWm-`=yre^pjyIwakuep6eOnTk@V`3X( za#@w4tv3Dzyp@rKW2DGrl8nv$1P8$mIi*2pLjac#*9L!7yAixySmWK*AD1w`+(&;k z1Pqnq|1wm*{k71&b4Ju+n*Ay~L0a>~<$>^aTHwseHCh23dK$}s(Mak?F^m1j@+w;w zO6F`=WkTe;q=&z$zp^bQWs-b)f8i~H-~8>@nz;MllPt<;0VdkNJEX|q6UP#%HXi%Z z06Q&qx{;;!Ont;;qa!6X`IRA!kWiR8n9={r^xXT)`M2f8?%KF7ly@FScF2c{BTFhbJ*xIH-652+O>YLS8 zPp2mhA@Ow+kFuU{R`!Z84rDAeb6-6v7s2?sLBRLoWomk}o{jaqJm+&rOXq6z(JY6O z=l*We;?4~Sna-Z-P?qZcY*h4iPp5;YRc4h4{<30NQ^#UP$Yq0Kw^zNNn7Cf{wDZ5zbdaOPsLLq}yA1$k+=pgC#>Mm5cIiDAY6M z>@7(vTjHWc-02KrC(}7ToJLu8u4}O~m&sq_`1|>!3uW!BbClNdZg`oi?yRJ%Fg&n% z=9D2EHt3S$MeWa5L&Cl_rJ@7RNwD=bf3}VG)h2m5cSh$Mdv$vd4UVgcR9^)Z?A?v3 zN96S@SM|r(s%fDW5iF9~QMOQTw^h7qZpO`yGNmWFFq*WFq@S3}x6kY46@;lRL8MNN zBBtB}Am>*;G8RN_1*`3F)iHKmVAS|>duGf%{?Yk#HuiR$?jSiJkHrP?1@lp-)a>5XzG6#Jx%5je5Tgvo$*xYQj0=1dK(G$lIZ`_B!eb`v4T&~$z_VU`n zH4-($!5^;*$ZUGb3^|pQCyw8ym5SSTbcHTyy7YKoRpS)vS)QO9tPp7a>T4^Mq50a; z1cXc#m3Z;c#@PhdzN%(}<&OhfsPjf^j0s_V9xYQ8XCLt`-41BOwrIF%^Yvltaa4`T z8dXBR{u%;RW4e|Ya7ua`HpWhNflv<#tKhVfx@VL1rom{#6RS=FOFQw6FR}^hDBshe zIO}(DOEtgx%Z3fsWZJ!q22)M4_V$FXaWef=i5lyH^6+d+#TJ&xM(;dMYi!QV?i-S4 zJ4KL)LKycPRxipg=3CjC z)=YCRu!+iSNmofRj}9)VzH#!a^}EzM_^UJ8hy|)FXvM2a0Smm#bh;)gc1@$umpy8_ zyh&-A=>{bV&B5ydlU-9W-KAB-XV>~a)WuLJ>vy9fG#^sQBCDn!M6?srUs1n{7Rcb> z`!mGeN1G+Mw3P5mRTQ7-#>U zx>VJVZ_?>f577s^H=dfh6d$(*3NI@(##yu_NtR2Uwq7hX+;MnIKcK9v!L*8el5}5A z;IVR@7no@dMof=Euc-uJPQ*Vtsgu_5u6BHR750`^vdfJ}--@3~U$42zIb zk#eNIvY=5?Vw3AW6D>C#ld*_hyZ!v)Oal(8f^|JvSMrqGgBF)eUuGfBlv6n5{wv|W z7woj_ctaJ0-ioN&4Jhn*5Ec4Tmxf42*b(>QqOJtg8`a5YYV@#)Jk-c)Z_m=lQny#E zJ3<>$%oZMHchsC!sYv+s$*^%K89$H`O#rvzSh>Jte!2H289k2HL7mGD#8^)Zt{Z;v zbS{i^tm;8S)MR@ziUiCQL%d^>tpZm$4djA8BoN#wno&@Vd@wCPzS=?EB1%Zm=3jxz z^ot|t`@@MREJ?elrq4GEGUfYdsfDqEWNYT$prXsk5=Ly1?3vW%v?rc|w!)h=1DhWd zEPPy=PP+{&b1ScO(iX4@;4p5J8) zyf-0pkH&T6u4-K)Gg^UP_oG{2_@o}^RA6-y6&-@6fF(aE8YR(0bZfZI36^(-sSVq= zdFai@ldxB%=W>Ow!kiziM;6wo5k$N9YNdVYKAzr>FWhjh7*TdTQ?ud9mUZs8&!{id z&acm?#Ec4d)vc@w3mj`Rs#hlDSRor{SY0zoBY7^A3{`EawFV-9*MtDPCZ_*z@4r8DLbNX1k^>2Xj>3v^Vb2IYT=%=g4QGg%3nINH>A`E= zeT3yjDR*$0b7fGoWS7?%&rA@>CmHet3yO7q*B`RxW{Pb%vDhZTBx)w~{(Sq(oC>)2nt_^-sh)Q$WMdktHW|PRX z%dcpqh7mHYXO75)GU|M@5F8dMG4&uv=PZ_`7H7H&E%8fdDAi3i@;9(YQw<2nbPA$^ zM%anTlNj~*;Pu@6Uo>Y$q>`_d$~Px`M#{Nbl*aXowQsq7=t8^fyxhT(pqdt(?LG1# z5r^loi*;sVZ=%jsq2<+9dyboyJQ-qWqLiF>B~GR6^D_@z=-Z~zk5ahY*|@u1?iIT| zT}bp;kg@(`md6tOr0kDZIH)tZ-`aMn%;R~i>mB^o)}=I^b`o#Yo|K`acA$q=(iZ75 z-zjM$jDH+NNEY4qq8*Z>2n@sfn<((r`Ppb>IShDYo-BLwlqx(*1 zMK?U`(4Cdcby0Wt7Kjk74t8RZ3TO~MlFtkkCY_-0R^_|vxjDaHti+`Zfm`AfqMtG=U_gP^L5fjUCS-NdtxPO^>7qWc9TRVAr$uQ(4scWot@P zU6+#aWl=0tDwK4Hn;=jia`+=BK%{73bHbq zTBns|F$j=95%z2BUH7RWkUjDt;4gc@6u+uL^mp~W8oPhh*bl$>Rn6l+)Z_s6Tn`A` zz<_-3GWnV=vkVaLfdz;u?49i0FX=vp2ZKmlb98b*8aw?t83%-NERFvmy#i*yvfezO z0Ne*4K&}vueR$xS?(+#4p#z%vPloR|2r%sS{qig(06-rIqdoHjY%gF72f#{MAotpB z>hQ0)S}=S)Zf^l4a0i0|6!0J5w#eaN-gl(`)Tc$be|3C15y8}3bwxl1peB{X~}cl z0CR4GLLe9SQGsi^Puq#Z0joKfnpxh!Wb`~Bp&gjGEGHtU3>c$qVCtp%199QW|DRYB z2mwc#I@#L%D7OG6k0H0`a|e3O3Ru3_59GZJ|2O&1jSZZbd!H@c2hH-^RDTB&fI9?E z!ir&G2h8OFtNXz|Fkj$-B;Z7c7!nxo?~)wSO2FCPFqARCF5K_+>+cvD@Zf=yGhs-S zfMAb9lN>m1-~=NW{29Rb4uSu3%L0oxV_?VS4+Zx?eHAm#TEZ8#tH5bSiBL#=Bx3$Y(MW0Sf&rdu&s4? z29RhUxEo-FISfgL&f!V+T%TV!Jz$M049S~Izen=xJ3m;;2g5^PczB+Jg?+$-2Nr_C z@SQXIUA~{&09cg+!$4+yXojCdgHK*zpabj=gg)RL7Ib19Cx*t+^EYS?e7J!_7cuyR-oJzYV<02A z7vS(13=6UUZ?GKv{et&DF$5vO|3Ls=vp>ic2Jc*9D9!?|_W_UTy%syN{EOnReJt=W vf%h6P6w?vEN%8YvEO;FWLx2M2=)da2J{~UMkMGUk;1D`e2xKaL@6-PPcY0!z literal 0 HcmV?d00001 diff --git a/plugins/spellchecker/spellchecker.iml b/plugins/spellchecker/spellchecker.iml index 80754d1ebb18..517fa99442d0 100644 --- a/plugins/spellchecker/spellchecker.iml +++ b/plugins/spellchecker/spellchecker.iml @@ -9,17 +9,6 @@ - - - - - - - - - - - diff --git a/plugins/spellchecker/src/META-INF/plugin.xml b/plugins/spellchecker/src/META-INF/plugin.xml index be891f33b0cb..59b8f91cdae5 100644 --- a/plugins/spellchecker/src/META-INF/plugin.xml +++ b/plugins/spellchecker/src/META-INF/plugin.xml @@ -9,18 +9,23 @@ - com.intellij.spellchecker.options.CachedDictionaryState - com.intellij.spellchecker.options.CachedDictionaryState + com.intellij.spellchecker.state.CachedDictionaryState + com.intellij.spellchecker.state.CachedDictionaryState - com.intellij.spellchecker.options.ProjectDictionaryState - com.intellij.spellchecker.options.ProjectDictionaryState + com.intellij.spellchecker.state.ProjectDictionaryState + com.intellij.spellchecker.state.ProjectDictionaryState + + + com.intellij.spellchecker.state.AggregatedDictionaryState + com.intellij.spellchecker.state.AggregatedDictionaryState + @@ -51,7 +56,7 @@ - + diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/FileLoader.java b/plugins/spellchecker/src/com/intellij/spellchecker/FileLoader.java new file mode 100644 index 000000000000..f22aec71f1b1 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/FileLoader.java @@ -0,0 +1,56 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker; + +import com.intellij.spellchecker.dictionary.Loader; +import com.intellij.spellchecker.dictionary.Processor; +import org.jetbrains.annotations.NotNull; + +import java.io.*; + + +public class FileLoader implements Loader { + + private String url; + + public FileLoader(String url) { + this.url = url; + } + + public void load(@NotNull Processor processor) { + + InputStream io = SpellCheckerManager.class.getResourceAsStream(url); + DataInputStream in = new DataInputStream(io); + BufferedReader br = new BufferedReader(new InputStreamReader(in)); + try{ + String strLine; + while ((strLine = br.readLine()) != null) { + processor.process(strLine); + } + in.close(); + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + br.close(); + } + catch (IOException ignored) { + + } + } + } + +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/SpellCheckerManager.java b/plugins/spellchecker/src/com/intellij/spellchecker/SpellCheckerManager.java index 5ae1687331ae..304701ae606f 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/SpellCheckerManager.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/SpellCheckerManager.java @@ -1,257 +1,148 @@ -/* - * Copyright 2000-2009 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.intellij.spellchecker; - -import com.intellij.codeHighlighting.HighlightDisplayLevel; -import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; -import com.intellij.openapi.application.ApplicationManager; -import com.intellij.openapi.components.ServiceManager; -import com.intellij.openapi.project.Project; -import com.intellij.openapi.project.ProjectManager; -import com.intellij.openapi.diagnostic.Logger; -import com.intellij.spellchecker.engine.SpellChecker; -import com.intellij.spellchecker.engine.SpellCheckerFactory; -import com.intellij.spellchecker.options.SpellCheckerConfiguration; -import com.intellij.spellchecker.options.ProjectDictionaryState; -import com.intellij.spellchecker.options.CachedDictionaryState; -import com.intellij.spellchecker.util.Strings; -import com.intellij.spellchecker.dictionary.Dictionary; -import org.jetbrains.annotations.NonNls; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.*; - -/** - * Spell checker inspection provider. - */ -public final class SpellCheckerManager { - private static final Logger LOG = Logger.getInstance("#com.intellij.spellchecker.SpellCheckerManager"); - private static final int MAX_SUGGESTIONS_THRESHOLD = 10; - private Project project; - private static HighlightDisplayLevel level; - private Set dictionaries = new HashSet(); - - private Dictionary projectWordList; - private Dictionary cachedWordList; - - public Set getDictionaries() { - return dictionaries; - } - - @NonNls - private static final String[] DICT_URLS = new String[]{"english.dic", "jetbrains.dic"}; - - - public static SpellCheckerManager getInstance(Project project) { - return ServiceManager.getService(project, SpellCheckerManager.class); - } - - private final SpellCheckerConfiguration configuration; - private final SpellChecker spellChecker = SpellCheckerFactory.create(); - - - public SpellCheckerManager(SpellCheckerConfiguration configuration, final Project project) { - this.configuration = configuration; - this.project = project; - reloadConfiguration(); - } - - @NotNull - public static HighlightDisplayLevel getHighlightDisplayLevel() { - return HighlightDisplayLevel.find(SpellCheckerSeveritiesProvider.TYPO); - } - - - @NotNull - public SpellChecker getSpellChecker() { - return spellChecker; - } - - - public boolean hasProblem(@NotNull String word) { - return !isIgnored(word) && !spellChecker.isCorrect(word); - } - - private boolean isIgnored(@NotNull String word) { - return spellChecker.isIgnored(word.toLowerCase()); - } - - public List getVariants(@NotNull String prefix) { - return spellChecker.getVariants(prefix); - } - - @NotNull - public List getSuggestions(@NotNull String word) { - if (!isIgnored(word) && !spellChecker.isCorrect(word)) { - List suggestions = spellChecker.getSuggestions(word, MAX_SUGGESTIONS_THRESHOLD); - if (suggestions.size() != 0) { - boolean capitalized = Strings.isCapitalized(word); - boolean upperCases = Strings.isUpperCase(word); - - if (capitalized) { - Strings.capitalize(suggestions); - } - else if (upperCases) { - Strings.upperCase(suggestions); - } - } - List result = new ArrayList(); - for (String s : suggestions) { - if (!result.contains(s)) { - result.add(s); - } - } - return result; - } - return Collections.emptyList(); - } - - @NotNull - public List getSuggestionsExt(@NotNull String word) { - return spellChecker.getSuggestionsExt(word, MAX_SUGGESTIONS_THRESHOLD); - } - - - /** - * Load dictionary from stream. - * - * @param inputStream Dictionary input stream - * @throws java.io.IOException if dictionary load with problems - */ - public void addDictionary(@NotNull InputStream inputStream) throws IOException { - addDictionary(inputStream, Charset.defaultCharset().name()); - } - - /** - * Load dictionary from stream. - * - * @param inputStream Dictionary input stream - * @param encoding Encoding - * @throws java.io.IOException if dictionary load with problems - */ - public void addDictionary(@NotNull InputStream inputStream, @NonNls String encoding) throws IOException { - addDictionary(inputStream, encoding, Locale.getDefault()); - } - - /** - * Load dictionary from stream. - * - * @param inputStream Dictionary input stream - * @param encoding Encoding - * @param locale Locale of dictionary - * @throws java.io.IOException if dictionary load with problems - */ - public void addDictionary(@NotNull InputStream inputStream, @NonNls String encoding, @NonNls @NotNull Locale locale) throws IOException { - spellChecker.addDictionary(inputStream, encoding, locale); - } - - public void acceptWordAsCorrect(@NotNull String word) { - String lowerCased = word.toLowerCase(); - projectWordList.acceptWord(word); - cachedWordList.acceptWord(word); - spellChecker.addToDictionary(lowerCased); - } - - public void reloadConfiguration() { - initDictionaries(); - spellChecker.reset(); - - projectWordList = ServiceManager.getService(project, ProjectDictionaryState.class).getDictionary(); - cachedWordList = ServiceManager.getService(project, CachedDictionaryState.class).getDictionary(); - - for (String word : projectWordList.getWords()) { - cachedWordList.acceptWord(word); - } - - - for (String word : ejectAll(cachedWordList.getWords())) { - String lowerCased = word.toLowerCase(); - spellChecker.addToDictionary(lowerCased); - } - - } - - public void applyConfiguration() { - initDictionaries(); - spellChecker.reset(); - - assert projectWordList != null; - assert cachedWordList != null; - - for (String word : projectWordList.getWords()) { - cachedWordList.acceptWord(word); - } - - - for (String word : ejectAll(cachedWordList.getWords())) { - String lowerCased = word.toLowerCase(); - spellChecker.addToDictionary(lowerCased); - } - - } - - private void initDictionaries() { - for (String dictUrl : DICT_URLS) { - initDictionary(dictUrl); - } - } - - private void initDictionary(String url) { - InputStream is = SpellCheckerManager.class.getResourceAsStream(url); - if (is != null) { - try { - dictionaries.add(url); - addDictionary(is); - } - catch (IOException e) { - LOG.error("Dictionary could not be loaded", e); - } - } - } - - public void restartInspections() { - //reloadConfiguration(); - ApplicationManager.getApplication().invokeLater(new Runnable() { - public void run() { - Project[] projects = ProjectManager.getInstance().getOpenProjects(); - for (Project project : projects) { - if (project.isInitialized() && project.isOpen() && !project.isDefault()) { - DaemonCodeAnalyzer.getInstance(project).restart(); - } - } - } - }); - } - - private HashSet ejectAll(Set from) { - HashSet words = new HashSet(from); - // from.clear(); - return words; - } - - public Dictionary getProjectWordList() { - return projectWordList; - } - - public Dictionary getCachedWordList() { - return cachedWordList; - } - - -} +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker; + +import com.intellij.codeHighlighting.HighlightDisplayLevel; +import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.project.ProjectManager; +import com.intellij.spellchecker.dictionary.Dictionary; +import com.intellij.spellchecker.dictionary.Loader; +import com.intellij.spellchecker.engine.SpellCheckerEngine; +import com.intellij.spellchecker.engine.SpellCheckerFactory; +import com.intellij.spellchecker.state.StateLoader; +import com.intellij.spellchecker.util.Strings; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class SpellCheckerManager { + + private static final int MAX_SUGGESTIONS_THRESHOLD = 5; + private static final int MAX_METRICS = 1; + + private Project project; + + private SpellCheckerEngine spellChecker; + + private Dictionary userDictionary; + + public static SpellCheckerManager getInstance(Project project) { + return ServiceManager.getService(project, SpellCheckerManager.class); + } + + public SpellCheckerManager(Project project) { + this.project = project; + reloadConfiguration(); + } + + public Dictionary getUserDictionary() { + return userDictionary; + } + + public void reloadConfiguration() { + spellChecker = SpellCheckerFactory.create(); + fillEngineDictionary(); + } + + private void fillEngineDictionary() { + spellChecker.reset(); + final StateLoader stateLoader = new StateLoader(project); + Loader[] loaders = new Loader[]{new FileLoader("english.dic"), new FileLoader("jetbrains.dic"), stateLoader}; + for (Loader loader : loaders) { + spellChecker.loadDictionary(loader); + } + userDictionary = stateLoader.getDictionary(); + } + + + public boolean hasProblem(@NotNull String word) { + return !spellChecker.isCorrect(word); + } + + public void acceptWordAsCorrect(@NotNull String word) { + final String transformed = spellChecker.getTransformation().transform(word); + if (transformed != null) { + userDictionary.addToDictionary(transformed); + spellChecker.addToDictionary(transformed); + } + } + + public void updateUserWords(@Nullable Collection words) { + Set transformed = spellChecker.getTransformation().transform(words); + userDictionary.replaceAll(transformed); + fillEngineDictionary(); + restartInspections(); + } + + + @NotNull + public static HighlightDisplayLevel getHighlightDisplayLevel() { + return HighlightDisplayLevel.find(SpellCheckerSeveritiesProvider.TYPO); + } + + + @NotNull + public List getSuggestions(@NotNull String word) { + if (!spellChecker.isCorrect(word)) { + List suggestions = spellChecker.getSuggestions(word, MAX_SUGGESTIONS_THRESHOLD, MAX_METRICS); + if (suggestions.size() != 0) { + boolean capitalized = Strings.isCapitalized(word); + boolean upperCases = Strings.isUpperCase(word); + if (capitalized) { + Strings.capitalize(suggestions); + } + else if (upperCases) { + Strings.upperCase(suggestions); + } + } + List result = new ArrayList(); + for (String s : suggestions) { + if (!result.contains(s)) { + result.add(s); + } + } + return result; + } + return Collections.emptyList(); + } + + @NotNull + public List getVariants(@NotNull String prefix) { + + return Collections.emptyList(); + } + + + public void restartInspections() { + ApplicationManager.getApplication().invokeLater(new Runnable() { + public void run() { + Project[] projects = ProjectManager.getInstance().getOpenProjects(); + for (Project project : projects) { + if (project.isInitialized() && project.isOpen() && !project.isDefault()) { + DaemonCodeAnalyzer.getInstance(project).restart(); + } + } + } + }); + } + + + +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/AggregatedDictionary.java b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/AggregatedDictionary.java new file mode 100644 index 000000000000..a5297adc0d37 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/AggregatedDictionary.java @@ -0,0 +1,120 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.dictionary; + +import com.intellij.spellchecker.trie.Action; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +public class AggregatedDictionary implements Dictionary { + + private static final String DICTIONARY_NAME = "common"; + private Dictionary cachedDictionary; + private ProjectDictionary projectDictionary; + + public String getName() { + return DICTIONARY_NAME; + } + + public AggregatedDictionary(@NotNull ProjectDictionary projectDictionary, @NotNull Dictionary cachedDictionary) { + this.projectDictionary = projectDictionary; + this.cachedDictionary = cachedDictionary; + this.cachedDictionary.addToDictionary(projectDictionary.getWords()); + } + + public boolean isEmpty() { + return false; + } + + public boolean contains(String word) { + if (word == null) { + return false; + } + return cachedDictionary.contains(word); + } + + public void addToDictionary(String word) { + getProjectDictionary().addToDictionary(word); + getCachedDictionary().addToDictionary(word); + } + + public void removeFromDictionary(String word) { + getProjectDictionary().removeFromDictionary(word); + getCachedDictionary().removeFromDictionary(word); + } + + + public void replaceAll(@Nullable Collection words) { + Set oldWords = getProjectDictionary().getWords(); + getProjectDictionary().replaceAll(words); + if (oldWords != null) { + for (String word : oldWords) { + if (words == null || !words.contains(word)) { + getCachedDictionary().removeFromDictionary(word); + } + } + } + } + + public void clear() { + getProjectDictionary().clear(); + } + + public void traverse(final Action action) { + cachedDictionary.traverse(action); + } + + + public Set getWords() { + Set words = new HashSet(); + words.addAll(cachedDictionary.getWords()); + return words; + } + + @Nullable + public Set getEditableWords() { + return getProjectDictionary().getEditableWords(); + } + + @Nullable + public Set getNotEditableWords() { + Set words = getWords(); + Set editable = getEditableWords(); + if (words != null && editable != null) { + words.removeAll(editable); + } + return words; + } + + public void addToDictionary(@Nullable Collection words) { + getProjectDictionary().addToDictionary(words); + getCachedDictionary().addToDictionary(words); + } + + public Dictionary getCachedDictionary() { + return cachedDictionary; + } + + public ProjectDictionary getProjectDictionary() { + return projectDictionary; + } + + +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Dictionary.java b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Dictionary.java index c961b5f7c5c2..d5b51f37d5ce 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Dictionary.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Dictionary.java @@ -1,32 +1,54 @@ -/* - * Copyright 2000-2009 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.intellij.spellchecker.dictionary; - -import org.jetbrains.annotations.NotNull; - -import java.util.Set; - - -public interface Dictionary { - - Set getWords(); - - void acceptWord(@NotNull String word); - - void replaceAllWords(Set newWords); - - String getName(); -} +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.dictionary; + +import com.intellij.spellchecker.trie.Action; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Set; + +public interface Dictionary { + + String getName(); + + boolean contains(String word); + + boolean isEmpty(); + + void addToDictionary(String word); + + void removeFromDictionary(String word); + + void addToDictionary(@Nullable Collection words); + + void replaceAll(@Nullable Collection words); + + void clear(); + + void traverse(final Action action); + + @Nullable + Set getWords(); + + @Nullable + Set getEditableWords(); + + @Nullable + Set getNotEditableWords(); + + +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Loader.java b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Loader.java new file mode 100644 index 000000000000..6a24dc79b2df --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Loader.java @@ -0,0 +1,24 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.dictionary; + +import org.jetbrains.annotations.NotNull; + + +public interface Loader { + + void load(@NotNull Processor processor); +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Processor.java b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Processor.java new file mode 100644 index 000000000000..22b6fab395c9 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/Processor.java @@ -0,0 +1,23 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.dictionary; + +import org.jetbrains.annotations.Nullable; + +public interface Processor { + + void process(@Nullable String word); +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/ProjectDictionary.java b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/ProjectDictionary.java index 1844f204576f..04b4156d3c9f 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/ProjectDictionary.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/ProjectDictionary.java @@ -1,89 +1,192 @@ -/* - * Copyright 2000-2009 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.intellij.spellchecker.dictionary; - -import com.intellij.util.containers.HashSet; -import org.jetbrains.annotations.NotNull; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -/** - * Created by IntelliJ IDEA. - * - * @author shkate@jetbrains.com - */ -public class ProjectDictionary extends UserDictionary { - - @NotNull - public List dictionaries = new ArrayList(); - - public ProjectDictionary() { - } - - public ProjectDictionary(String name) { - super(name); - } - - @Override - public Set getWords() { - Set words = new HashSet(); - for (Dictionary dictionary : dictionaries) { - words.addAll(dictionary.getWords()); - } - return words; - } - - @Override - public void acceptWord(@NotNull String word) { - getUserDictionary().acceptWord(word); - } - - @Override - public void replaceAllWords(Set newWords) { - getUserDictionary().replaceAllWords(newWords); - } - - public void setDictionaries(@NotNull List dictionaries) { - this.dictionaries = dictionaries; - } - - @NotNull - public List getDictionaries(){ - return dictionaries; - } - - public Dictionary getUserDictionary() { - final String name = getCurrentUserName(); - Dictionary userDictionary = null; - for (Dictionary dictionary : dictionaries) { - if (dictionary.getName().equals(name)){ - userDictionary = dictionary; - break; - } - } - if (userDictionary==null){ - userDictionary = new UserDictionary(name); - dictionaries.add((UserDictionary)userDictionary); - } - return userDictionary; - } - - public static String getCurrentUserName() { - return System.getProperty("user.name"); - } -} +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.dictionary; + +import com.intellij.spellchecker.trie.Action; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.*; + +public class ProjectDictionary implements Dictionary { + + private static final String DEFAULT_CURRENT_USER_NAME = "default.user"; + private static final String DEFAULT_PROJECT_DICTIONARY_NAME = "project"; + private String activeName; + private Set dictionaries; + + + public ProjectDictionary() { + } + + public ProjectDictionary(Set dictionaries) { + this.dictionaries = dictionaries; + } + + public boolean isEmpty() { + return false; + } + + public String getName() { + return DEFAULT_PROJECT_DICTIONARY_NAME; + } + + public String getActiveName() { + return activeName; + } + + public void setActiveName(String name) { + this.activeName = name; + } + + public boolean contains(String word) { + if (word == null || dictionaries == null) { + return false; + } + for (Dictionary dictionary : dictionaries) { + if (dictionary.contains(word)) { + return true; + } + } + return false; + } + + public void addToDictionary(String word) { + getActiveDictionary().addToDictionary(word); + } + + public void removeFromDictionary(String word) { + getActiveDictionary().removeFromDictionary(word); + } + + @NotNull + private Dictionary getActiveDictionary() { + return ensureCurrentUserDictionary(); + } + + @NotNull + private Dictionary ensureCurrentUserDictionary() { + if (activeName == null) { + activeName = DEFAULT_CURRENT_USER_NAME; + } + Dictionary result = getDictionaryByName(activeName); + if (result == null) { + result = new UserDictionary(this.activeName); + if (dictionaries == null) { + dictionaries = new HashSet(); + } + dictionaries.add(result); + } + return result; + } + + @Nullable + private Dictionary getDictionaryByName(@NotNull String name) { + if (dictionaries == null) { + return null; + } + Dictionary result = null; + for (Dictionary dictionary : dictionaries) { + if (dictionary.getName().equals(name)) { + result = dictionary; + break; + } + } + return result; + } + + + public void replaceAll(@Nullable Collection words) { + getActiveDictionary().replaceAll(words); + } + + public void clear() { + getActiveDictionary().clear(); + } + + + @Nullable + public Set getWords() { + if (dictionaries == null) { + return null; + } + Set words = new HashSet(); + for (Dictionary dictionary : dictionaries) { + words.addAll(dictionary.getWords()); + } + return words; + } + + public void traverse(Action action) { + if (dictionaries == null) { + return; + } + + for (Dictionary dictionary : dictionaries) { + dictionary.traverse(action); + } + + } + + @Nullable + public Set getEditableWords() { + return getActiveDictionary().getWords(); + } + + @Nullable + public Set getNotEditableWords() { + Set words = getWords(); + Set editable = getEditableWords(); + if (words != null && editable != null) { + words.removeAll(editable); + } + return words; + } + + public void addToDictionary(@Nullable Collection words) { + getActiveDictionary().addToDictionary(words); + } + + public Set getDictionaries() { + return dictionaries; + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ProjectDictionary that = (ProjectDictionary)o; + + if (activeName != null ? !activeName.equals(that.activeName) : that.activeName != null) return false; + if (dictionaries != null ? !dictionaries.equals(that.dictionaries) : that.dictionaries != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = activeName != null ? activeName.hashCode() : 0; + result = 31 * result + (dictionaries != null ? dictionaries.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "ProjectDictionary{" + "activeName='" + activeName + '\'' + ", dictionaries=" + dictionaries + '}'; + } +} \ No newline at end of file diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/UserDictionary.java b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/UserDictionary.java index 7c5db10f6b9a..da8f576c5395 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/UserDictionary.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/dictionary/UserDictionary.java @@ -1,73 +1,126 @@ -/* - * Copyright 2000-2009 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.intellij.spellchecker.dictionary; - -import com.intellij.util.xmlb.annotations.AbstractCollection; -import com.intellij.util.xmlb.annotations.Attribute; -import com.intellij.util.xmlb.annotations.Tag; -import org.jetbrains.annotations.NotNull; - -import java.util.Collections; -import java.util.HashSet; -import java.util.Set; - -/** - * Created by IntelliJ IDEA. - * - * @author shkate@jetbrains.com - */ -@Tag("dictionary") -public class UserDictionary implements Dictionary { - - @Tag("words") - @AbstractCollection(surroundWithTag = false,elementTag = "w",elementValueAttribute = "") - public Set words = new HashSet(); - - @Attribute(NAME_ATTRIBUTE) - public String name = "new"; - private static final String NAME_ATTRIBUTE = "name"; - - public UserDictionary() { - } - - public UserDictionary(String name) { - this.name = name; - } - - @NotNull - public String getName() { - return name; - } - - - public Set getWords() { - return Collections.unmodifiableSet(words); - } - - public void acceptWord(@NotNull String word) { - words.add(word); - } - - public void replaceAllWords(Set newWords) { - replaceAll(words, newWords); - } - - private static void replaceAll(Set words, Set newWords) { - words.clear(); - words.addAll(newWords); - } - -} +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.dictionary; + +import com.intellij.spellchecker.trie.Action; +import com.intellij.spellchecker.trie.CharSequenceKeyAnalyzer; +import com.intellij.spellchecker.trie.PatriciaTrie; +import com.intellij.spellchecker.trie.Trie; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +public class UserDictionary implements Dictionary { + + private String name; + private Trie trie = new PatriciaTrie(new CharSequenceKeyAnalyzer()); + + public UserDictionary(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public boolean contains(String word) { + return word != null && trie.containsKey(word); + } + + @Nullable + public Set getWords() { + return trie.keySet(); + } + + @Nullable + public Set getEditableWords() { + return trie.keySet(); + } + + @Nullable + public Set getNotEditableWords() { + return null; + } + + public void clear() { + trie.clear(); + } + + + public void addToDictionary(String word) { + if (word == null) { + return; + } + trie.put(word, word); + } + + public void removeFromDictionary(String word) { + if (word == null) { + return; + } + trie.remove(word); + } + + public void replaceAll(@Nullable Collection words) { + clear(); + addToDictionary(words); + } + + public void addToDictionary(@Nullable Collection words) { + if (words == null || words.isEmpty()) { + return; + } + for (String word : words) { + addToDictionary(word); + } + } + + public boolean isEmpty() { + return (trie == null || trie.size() == 0); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + UserDictionary that = (UserDictionary)o; + + return !(name != null ? !name.equals(that.name) : that.name != null); + + } + + public void traverse(final Action action){ + trie.traverse(new PatriciaTrie.Cursor() { + public SelectStatus select(Map.Entry entry) { + action.run(entry); + return SelectStatus.CONTINUE; + } + }); + } + + @Override + public int hashCode() { + return name != null ? name.hashCode() : 0; + } + + @Override + public String toString() { + return "UserDictionary{" + "name='" + name + '\'' + ", words.count=" + trie.size() + '}'; + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/engine/BaseSpellChecker.java b/plugins/spellchecker/src/com/intellij/spellchecker/engine/BaseSpellChecker.java new file mode 100644 index 000000000000..ef5532fe1ea6 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/engine/BaseSpellChecker.java @@ -0,0 +1,115 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.engine; + +import com.intellij.spellchecker.dictionary.*; +import com.intellij.spellchecker.trie.Action; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + + +public class BaseSpellChecker implements SpellCheckerEngine { + + private Dictionary engineDictionary; + + private Transformation transform = new Transformation(); + + private Metrics metrics = new LevenshteinDistance(); + + private Processor processor = new Processor() { + public void process(@Nullable String word) { + final String transformed = transform.transform(word); + if (transformed != null) { + engineDictionary.addToDictionary(transformed); + } + } + }; + + public BaseSpellChecker() { + ensureEngineDictionary(); + } + + public void loadDictionary(@NotNull Loader loader) { + loader.load(processor); + } + + public Transformation getTransformation() { + return transform; + } + + private void ensureEngineDictionary() { + if (engineDictionary == null) { + engineDictionary = new UserDictionary("engine"); + } + } + + + public void addToDictionary(String word) { + final String transformed = transform.transform(word); + if (transformed != null) { + engineDictionary.addToDictionary(transformed); + } + } + + public boolean isCorrect(@NotNull String word) { + final String transformed = transform.transform(word); + return transformed != null && engineDictionary.contains(transformed); + } + + @NotNull + public List getSuggestions(final @NotNull String word, int threshold, int quality) { + final List suggestions = new ArrayList(); + + suggestions.clear(); + engineDictionary.traverse(new Action() { + public void run(Map.Entry entry) { + if (word.charAt(0) == entry.getKey().charAt(0)/* && Math.abs(mpw.length() - entry.getKey().length()) <= 1*/) { + final int distance = metrics.calculateMetrics(word, entry.getKey()); + suggestions.add(new Suggestion(entry.getKey(), distance)); + } + } + }); + List result = new ArrayList(); + if (suggestions.isEmpty()){ + return result; + } + Collections.sort(suggestions); + int bestMetrics = suggestions.get(0).getMetrics(); + for (int i = 0; i < threshold; i++) { + + if (suggestions.size()quality){ + break; + } + result.add(i,suggestions.get(i).getWord()); + } + return result; + } + + @NotNull + public List getVariants(@NotNull String prefix) { + return null; + } + + public void reset() { + engineDictionary = null; + ensureEngineDictionary(); + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/engine/JazzySpellChecker.java b/plugins/spellchecker/src/com/intellij/spellchecker/engine/JazzySpellChecker.java deleted file mode 100644 index a5ee8e24fecd..000000000000 --- a/plugins/spellchecker/src/com/intellij/spellchecker/engine/JazzySpellChecker.java +++ /dev/null @@ -1,286 +0,0 @@ -/* - * Copyright 2000-2009 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.intellij.spellchecker.engine; - -import com.swabunga.spell.engine.SpellDictionaryHashMap; -import com.swabunga.spell.engine.Word; -import com.intellij.psi.codeStyle.NameUtil; -import org.jetbrains.annotations.NonNls; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.Reader; -import java.util.*; - -/** - * Jazzy implementation of Spell Checker. - */ -final class JazzySpellChecker implements SpellChecker { - private final SpellCheckerWrapper delegate = new SpellCheckerWrapper(); - private final Map> dictionaries = new HashMap>(); - private final Set allowed = new HashSet(); - private SpellDictionaryImpl userDictionary; - - JazzySpellChecker() { - setUserDictionary(); - } - - public void addDictionary(@NotNull InputStream is, @NonNls String encoding, @NotNull Locale locale) throws IOException { - SpellDictionaryImpl spellDictionary = new SpellDictionaryImpl(new InputStreamReader(is, encoding), locale); - Set indexedChars = spellDictionary.getIndexedChars(); - dictionaries.put(spellDictionary, indexedChars); - allowed.addAll(indexedChars); - delegate.addDictionary(spellDictionary); - } - - public void addToDictionary(@NotNull String word) { - delegate.addToDictionary(word); - indexWord(word); - } - - private void indexWord(CharSequence word) { - indexWord(word, allowed); - } - - private static void indexWord(CharSequence word, Set index) { - for (int i = 0; i < word.length(); i++) { - index.add(word.charAt(i)); - } - } - - public void ignoreAll(@NotNull String word) { - delegate.ignoreAll(word); - } - - public boolean isIgnored(@NotNull String word) { - return !isEntireWordAllowed(word, allowed) || delegate.isIgnored(word); - } - - public boolean isCorrect(@NotNull String word) { - return !isEntireWordAllowed(word, allowed) || delegate.isCorrect(word); - } - - private static boolean isEntireWordAllowed(CharSequence word, Set index) { - for (int i = 0; i < word.length(); i++) { - if (!index.contains(word.charAt(i))) { - return false; - } - } - return true; - } - - private Set findDictionaryIndex(String word) { - Set commonIndex = null; - Set lastIndex = null; - Collection> indexes = dictionaries.values(); - for (Set index : indexes) { - if (isEntireWordAllowed(word, index)) { - if (lastIndex != null && commonIndex == null) { - commonIndex = new HashSet(lastIndex.size() + index.size()); - commonIndex.addAll(lastIndex); - lastIndex = commonIndex; - } - if (commonIndex != null) { - commonIndex.addAll(index); - } else { - lastIndex = index; - } - } - } - return lastIndex; - } - - @NotNull - @SuppressWarnings({"unchecked"}) - public List getSuggestions(@NotNull String word, int threshold) { - List words = delegate.getSuggestions(word, threshold); - Set index = findDictionaryIndex(word); - List strings = new ArrayList(words.size()); - for (Word w : words) { - String suggestion = w.getWord(); - if (index == null || isEntireWordAllowed(suggestion, index)) { - strings.add(suggestion); - } - } - return strings; - } - - @NotNull - public List getSuggestionsExt(@NotNull String text, int threshold) { - String[] words = NameUtil.nameToWords(text); - List result = new ArrayList(); - int index = 0; - List[] res = new List[words.length]; - int i = 0; - for (String word : words) { - int start = text.indexOf(word, index); - int end = start + word.length(); - if (!isCorrect(word)) { - List variants = new ArrayList(); - variants.add(word); - res[i++] = variants; - } else { - List variants = getSuggestions(word, threshold); - res[i++] = variants; - } - index = end; - } - - - int counter[] = new int[i]; - int size = 1; - for (int j = 0; j < i; j++) { - size *= res[j].size(); - } - String[] all = new String[size]; - - for (int k = 0; k < size; k++) { - for (int j = 0; j < i; j++) { - if (all[k] == null) { - all[k] = ""; - } - all[k] += res[j].get(counter[j]); - counter[j]++; - if (counter[j] >= res[j].size()) { - counter[j] = 0; - } - } - } - - result.addAll(Arrays.asList(all)); - return result; - } - - @NotNull - public List getVariants(@NotNull String prefix) { - if (prefix.length() > 0) { - List variants = new ArrayList(); - userDictionary.appendWordsStartsWith(prefix, variants); - Set index = new HashSet(); - indexWord(prefix, index); - for (SpellDictionaryImpl dictionary : dictionaries.keySet()) { - Set dictionaryIndex = dictionaries.get(dictionary); - if (isSame(index, dictionaryIndex)) { - dictionary.appendWordsStartsWith(prefix, variants); - } - } - Collections.sort(variants); - return variants; - } - return Collections.emptyList(); - } - - private static boolean isSame(@NotNull Set i1, @NotNull Set i2) { - if (i1.equals(i2)) { - return true; - } - for (Character c : i1) { - if (!i2.contains(c)) { - return false; - } - } - return true; - } - - public void reset() { - delegate.reset(); - allowed.clear(); - Collection> sets = dictionaries.values(); - for (Set set : sets) { - allowed.addAll(set); - } - setUserDictionary(); - } - - private void setUserDictionary() { - try { - userDictionary = new SpellDictionaryImpl(Locale.getDefault()); - delegate.setUserDictionary(userDictionary); - } - catch (IOException e) { - throw new RuntimeException(e); - } - } - - private static class SpellDictionaryImpl extends SpellDictionaryHashMap { - private final Locale locale; - - public SpellDictionaryImpl(Locale locale) throws IOException { - this.locale = locale; - } - - private SpellDictionaryImpl(Reader wordList, Locale locale) throws IOException { - super(wordList); - this.locale = locale; - } - - @SuppressWarnings({"unchecked"}) - public Set getIndexedChars() { - Set index = new HashSet(64); - Collection> values = mainDictionary.values(); - for (List wordList : values) { - if (wordList != null) { - for (String word : wordList) { - if (word != null) { - indexWord(word, index); - } - } - } - } - return Collections.unmodifiableSet(index); - } - - @SuppressWarnings({"unchecked"}) - public void appendWordsStartsWith(@NotNull String prefix, @NotNull Collection buffer) { - String prefixLowerCase = prefix.toLowerCase(locale); - Collection> values = mainDictionary.values(); - int prefixLength = prefix.length(); - StringBuilder builder = new StringBuilder(); - for (List wordList : values) { - if (wordList != null) { - for (String word : wordList) { - if (word != null) { - String lowerCased = word.toLowerCase(locale); - int length = lowerCased.length(); - if (lowerCased.startsWith(prefixLowerCase) && length > prefixLength) { - builder.setLength(0); - builder.append(prefix); - builder.append(lowerCased, prefixLength, length); - String value = builder.toString(); - if (!buffer.contains(value)) { - buffer.add(value); - } - } - } - } - } - } - } - } - - private static final class SpellCheckerWrapper extends com.swabunga.spell.event.SpellChecker { - private SpellCheckerWrapper() { - // Disable caching - setCache(0); - } - - public List getSuggestions(String word, int threshold) { - return super.getSuggestions(word, threshold); - } - } -} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/engine/LevenshteinDistance.java b/plugins/spellchecker/src/com/intellij/spellchecker/engine/LevenshteinDistance.java new file mode 100644 index 000000000000..395e4ab86f9e --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/engine/LevenshteinDistance.java @@ -0,0 +1,43 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.engine; + +public class LevenshteinDistance implements Metrics { + + private static int minimum(int a, int b, int c) { + return Math.min(Math.min(a, b), c); + } + + public int calculateMetrics(CharSequence str1, CharSequence str2) { + int[][] distance = new int[str1.length() + 1][str2.length() + 1]; + + for (int i = 0; i <= str1.length(); i++) { + distance[i][0] = i; + } + for (int j = 0; j <= str2.length(); j++) { + distance[0][j] = j; + } + + for (int i = 1; i <= str1.length(); i++) { + for (int j = 1; j <= str2.length(); j++) { + distance[i][j] = minimum(distance[i - 1][j] + 1, distance[i][j - 1] + 1, + distance[i - 1][j - 1] + ((str1.charAt(i - 1) == str2.charAt(j - 1)) ? 0 : 1)); + } + } + + return distance[str1.length()][str2.length()]; + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/engine/Metrics.java b/plugins/spellchecker/src/com/intellij/spellchecker/engine/Metrics.java new file mode 100644 index 000000000000..c91a117cf90d --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/engine/Metrics.java @@ -0,0 +1,22 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.engine; + + +public interface Metrics { + + int calculateMetrics(CharSequence str1, CharSequence str2); +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellChecker.java b/plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellCheckerEngine.java similarity index 66% rename from plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellChecker.java rename to plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellCheckerEngine.java index e81a56e96de8..1afeb61ffb96 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellChecker.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellCheckerEngine.java @@ -1,53 +1,46 @@ -/* - * Copyright 2000-2009 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.intellij.spellchecker.engine; - -import org.jetbrains.annotations.NonNls; -import org.jetbrains.annotations.NotNull; - -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import java.util.Locale; - -/** - * Spell checker. - */ -public interface SpellChecker { - void addDictionary(@NotNull InputStream is, @NonNls String encoding, @NotNull Locale locale) throws IOException; - - void addToDictionary(@NotNull String word); - - void ignoreAll(@NotNull String word); - - boolean isIgnored(@NotNull String word); - - boolean isCorrect(@NotNull String word); - - @NotNull - List getSuggestions(@NotNull String word, int threshold); - - @NotNull - List getSuggestionsExt(@NotNull String word, int threshold); - - @NotNull - List getVariants(@NotNull String prefix); - - /** - * This method must clean up user dictionary words and ignored words. - */ - void reset(); -} +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.engine; + +import com.intellij.spellchecker.dictionary.Loader; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + + +public interface SpellCheckerEngine { + + + void loadDictionary(@NotNull Loader loader); + + Transformation getTransformation(); + + boolean isCorrect(@NotNull String word); + + void addToDictionary(String word); + + @NotNull + List getSuggestions(@NotNull String word, int threshold, int quality); + + @NotNull + List getVariants(@NotNull String prefix); + + /** + * This method must clean up user dictionary words and ignored words. + */ + void reset(); + +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellCheckerFactory.java b/plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellCheckerFactory.java index 06cfc7a6f490..f809ee7828a3 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellCheckerFactory.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/engine/SpellCheckerFactory.java @@ -1,28 +1,25 @@ -/* - * Copyright 2000-2009 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.intellij.spellchecker.engine; - -/** - * Spell checker factory. - */ -public final class SpellCheckerFactory { - private SpellCheckerFactory() { - } - - public static SpellChecker create() { - return new JazzySpellChecker(); - } -} +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.engine; + +public final class SpellCheckerFactory { + private SpellCheckerFactory() { + } + + public static SpellCheckerEngine create() { + return new BaseSpellChecker(); + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/engine/Suggestion.java b/plugins/spellchecker/src/com/intellij/spellchecker/engine/Suggestion.java new file mode 100644 index 000000000000..85b0ee2a7060 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/engine/Suggestion.java @@ -0,0 +1,65 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.engine; + +public class Suggestion implements Comparable{ + private String word; + private int metrics; + + public Suggestion(String word, int metrics) { + this.word = word; + this.metrics = metrics; + } + + public String getWord() { + return word; + } + + + public int getMetrics() { + return metrics; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Suggestion result = (Suggestion)o; + + if (metrics != result.metrics) return false; + if (word != null ? !word.equals(result.word) : result.word != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = word != null ? word.hashCode() : 0; + result = 31 * result + metrics; + return result; + } + + + public int compareTo(Object o) { + if (!(o instanceof Suggestion)) throw new IllegalArgumentException(); + Suggestion r = (Suggestion)o; + return new Integer(getMetrics()).compareTo(r.getMetrics()); + } +} + + + diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/engine/Transformation.java b/plugins/spellchecker/src/com/intellij/spellchecker/engine/Transformation.java new file mode 100644 index 000000000000..b35d6e187088 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/engine/Transformation.java @@ -0,0 +1,48 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.engine; + +import com.intellij.util.containers.hash.HashSet; +import org.jetbrains.annotations.Nullable; + +import java.util.Collection; +import java.util.Set; + +public class Transformation { + + @Nullable + public String transform(@Nullable String word) { + if (word == null || word.trim().length() < 3) { + return null; + } + return word.trim().toLowerCase(); + } + + @Nullable + public Set transform(@Nullable Collection words) { + if (words == null || words.isEmpty()) { + return null; + } + Set result = new HashSet(); + for (String word : words) { + String transformed = transform(word); + if (transformed != null) { + result.add(transformed); + } + } + return result; + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/CheckArea.java b/plugins/spellchecker/src/com/intellij/spellchecker/inspections/CheckArea.java similarity index 85% rename from plugins/spellchecker/src/com/intellij/spellchecker/CheckArea.java rename to plugins/spellchecker/src/com/intellij/spellchecker/inspections/CheckArea.java index c3241600f840..e3075f94669e 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/CheckArea.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/inspections/CheckArea.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.intellij.spellchecker; +package com.intellij.spellchecker.inspections; import com.intellij.openapi.util.TextRange; import org.jetbrains.annotations.Nullable; @@ -39,17 +39,11 @@ public class CheckArea { return textRange; } - public void setTextRange(TextRange textRange) { - this.textRange = textRange; - } public boolean isIgnored() { return ignored; } - public void setIgnored(boolean ignored) { - this.ignored = ignored; - } @Nullable public String getWord() { @@ -59,6 +53,6 @@ public class CheckArea { @Override public String toString() { - return "CheckArea{range = " + textRange + ", ignored=" + ignored + ", word=" + (getWord()!=null?getWord():"") +'}'; + return "CheckArea{range = " + textRange + ", ignored=" + ignored + ", word=" + (getWord() != null ? getWord() : "") + '}'; } } diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/inspections/SpellCheckingInspection.java b/plugins/spellchecker/src/com/intellij/spellchecker/inspections/SpellCheckingInspection.java index cefe31399200..07459ca9cc14 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/inspections/SpellCheckingInspection.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/inspections/SpellCheckingInspection.java @@ -26,9 +26,7 @@ import com.intellij.openapi.util.TextRange; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiElementVisitor; import com.intellij.psi.tree.IElementType; -import com.intellij.spellchecker.CheckArea; import com.intellij.spellchecker.SpellCheckerManager; -import com.intellij.spellchecker.TextSplitter; import com.intellij.spellchecker.quickfixes.AcceptWordAsCorrect; import com.intellij.spellchecker.quickfixes.ChangeTo; import com.intellij.spellchecker.quickfixes.RenameTo; @@ -180,7 +178,7 @@ public class SpellCheckingInspection extends LocalInspectionTool { fixes.add(new ChangeTo(textRange, word, token.getElement().getProject())); } else { - fixes.add(new RenameTo()); + fixes.add(new RenameTo(textRange, word, token.getElement().getProject())); } } @@ -201,6 +199,7 @@ public class SpellCheckingInspection extends LocalInspectionTool { final String description = tokenDescription == null ? defaultDescription : tokenDescription; final TextRange highlightRange = TextRange.from(token.getOffset() + textRange.getStartOffset(), textRange.getLength()); final LocalQuickFix[] quickFixes = fixes.size() > 0 ? fixes.toArray(new LocalQuickFix[fixes.size()]) : null; + return holder.getManager() .createProblemDescriptor(token.getElement(), highlightRange, description, ProblemHighlightType.GENERIC_ERROR_OR_WARNING, quickFixes); } diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/TextSplitter.java b/plugins/spellchecker/src/com/intellij/spellchecker/inspections/TextSplitter.java similarity index 98% rename from plugins/spellchecker/src/com/intellij/spellchecker/TextSplitter.java rename to plugins/spellchecker/src/com/intellij/spellchecker/inspections/TextSplitter.java index fb7a3dabe339..6f48b221e8c6 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/TextSplitter.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/inspections/TextSplitter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.intellij.spellchecker; +package com.intellij.spellchecker.inspections; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; @@ -41,7 +41,7 @@ public class TextSplitter { @NonNls /*private static final Pattern WORD = Pattern.compile("\\b\\p{L}+'?\\p{L}*\\b");*/ - private static final Pattern WORD = Pattern.compile("\\b\\p{Alpha}*'?\\p{Alpha}"); + private static final Pattern WORD = Pattern.compile("\\b\\p{Alpha}*'?\\p{Alpha}*"); private static final Pattern EXTENDED_WORD = Pattern.compile("\\b\\p{Alpha}*'?\\p{Alpha}(_*\\p{Alpha})*"); diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/options/ProjectDictionaryState.java b/plugins/spellchecker/src/com/intellij/spellchecker/options/ProjectDictionaryState.java deleted file mode 100644 index 9e40c088ab7b..000000000000 --- a/plugins/spellchecker/src/com/intellij/spellchecker/options/ProjectDictionaryState.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2000-2009 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.intellij.spellchecker.options; - -import com.intellij.openapi.components.PersistentStateComponent; -import com.intellij.openapi.components.State; -import com.intellij.openapi.components.Storage; -import com.intellij.openapi.components.StorageScheme; -import com.intellij.util.xmlb.XmlSerializerUtil; -import com.intellij.util.xmlb.annotations.AbstractCollection; -import com.intellij.util.xmlb.annotations.Property; -import com.intellij.spellchecker.dictionary.UserDictionary; -import com.intellij.spellchecker.dictionary.Dictionary; -import com.intellij.spellchecker.dictionary.ProjectDictionary; - -import java.util.ArrayList; -import java.util.List; - -@State( - name = "ProjectDictionaryState", - storages = {@Storage( - id = "other", - file = "$PROJECT_FILE$"), @Storage( - id = "dir", - file = "$PROJECT_CONFIG_DIR$/dictionaries/", - scheme = StorageScheme.DIRECTORY_BASED, stateSplitter = ProjectDictionarySplitter.class)}) - -public class ProjectDictionaryState implements PersistentStateComponent { - - @Property(surroundWithTag = false) - @AbstractCollection(surroundWithTag = false,elementTypes = UserDictionary.class) - public List dictionaries = new ArrayList(); - - - private ProjectDictionary projectDictionary; - - - - public ProjectDictionaryState getState() { - return this; - } - - - public void loadState(ProjectDictionaryState state) { - XmlSerializerUtil.copyBean(state, this); - createProjectDictionary(); - } - - private void createProjectDictionary() { - projectDictionary = new ProjectDictionary("project"); - projectDictionary.setDictionaries(dictionaries); - projectDictionary.getUserDictionary(); - } - - public ProjectDictionary getDictionary() { - if (projectDictionary == null) { - createProjectDictionary(); - } - return projectDictionary; - } -} - diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerConfigurable.java b/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerConfigurable.java index 0ebbd1113ecf..57e5093a0b8d 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerConfigurable.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerConfigurable.java @@ -1,120 +1,102 @@ -/* - * Copyright 2000-2009 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.intellij.spellchecker.options; - -import com.intellij.openapi.options.Configurable; -import com.intellij.openapi.options.ConfigurationException; -import com.intellij.openapi.project.Project; -import com.intellij.spellchecker.SpellCheckerManager; -import com.intellij.spellchecker.util.SpellCheckerBundle; -import org.jetbrains.annotations.Nls; -import org.jetbrains.annotations.NonNls; -import org.jetbrains.annotations.Nullable; - -import javax.swing.*; -import java.util.Set; - - -public final class SpellCheckerConfigurable implements Configurable { - private SpellCheckerManager manager; - private final Project myProject; - private final SpellCheckerConfiguration configuration; - - private SpellCheckerOptions options; - - public SpellCheckerConfigurable(Project project, SpellCheckerConfiguration configuration) { - myProject = project; - this.configuration = configuration; - } - - @Nls - public String getDisplayName() { - return SpellCheckerBundle.message("spelling"); - } - - @Nullable - public Icon getIcon() { - return null; - } - - @Nullable - @NonNls - public String getHelpTopic() { - return "reference.settings.ide.settings.spelling"; - } - - public JComponent createComponent() { - manager = SpellCheckerManager.getInstance(myProject); - if (options == null) { - options = new SpellCheckerOptions(configuration, manager); - } - - return options.getRoot(); - } - - public boolean isModified() { - if (options != null) { - if (dictionaryListWasChanged()) { - return true; - } - - } - return false; - } - - - private boolean dictionaryListWasChanged() { - return !same(options.getUserDictionaryWordsSet(), options.getShownDictionary().getWords()); - } - - - private static boolean same(Set modified, Set original) { - if (original.size() != modified.size()) { - return false; - } - modified.removeAll(original); - return modified.size() == 0; - } - - public void apply() throws ConfigurationException { - if (options != null) { - boolean reload = false; - if (dictionaryListWasChanged()) { - options.getShownDictionary().replaceAllWords(options.getUserDictionaryWordsSet()); - reload = true; - } - - /***replaceAll(configuration.activeDictionary.dictionaryWords, options.getUserDictionaryWords()); - replaceAll(configuration.activeDictionary.ignoredWords, options.getIgnoredWords());*//**//* - manager.restartInspections();*/ - if (reload) { - manager.applyConfiguration(); - manager.restartInspections(); - } - } - } - - - public void reset() { - } - - public void disposeUIResources() { - if (options != null) { - options.dispose(); - options = null; - } - } -} +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.options; + +import com.intellij.openapi.options.Configurable; +import com.intellij.openapi.options.ConfigurationException; +import com.intellij.openapi.project.Project; +import com.intellij.spellchecker.SpellCheckerManager; +import com.intellij.spellchecker.util.SpellCheckerBundle; +import org.jetbrains.annotations.Nls; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.List; +import java.util.Set; + + +public final class SpellCheckerConfigurable implements Configurable { + private SpellCheckerOptions options; + private SpellCheckerManager manager; + private final Project myProject; + + + public SpellCheckerConfigurable(Project project) { + myProject = project; + } + + @Nls + public String getDisplayName() { + return SpellCheckerBundle.message("spelling"); + } + + @Nullable + public Icon getIcon() { + return null; + } + + @Nullable + @NonNls + public String getHelpTopic() { + return "reference.settings.ide.settings.spelling"; + } + + public JComponent createComponent() { + manager = SpellCheckerManager.getInstance(myProject); + if (options == null) { + options = new SpellCheckerOptions(manager); + } + return options.getRoot(); + } + + public boolean isModified() { + if (options != null) { + return wordsListIsModified(); + } + return false; + } + + + private boolean wordsListIsModified() { + assert options != null; + List newWords = options.getWords(); + Set words = manager.getUserDictionary().getWords(); + if (words == null && newWords == null) { + return false; + } + if (words == null || newWords == null || newWords.size() != words.size()) { + return true; + } + return words.containsAll(newWords) && newWords.containsAll(words); + } + + + public void apply() throws ConfigurationException { + manager.updateUserWords(options.getWords()); + } + + + public void reset() { + } + + public void disposeUIResources() { + if (options != null) { + options.dispose(); + options = null; + } + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerConfiguration.java b/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerConfiguration.java deleted file mode 100644 index ed359e586ac9..000000000000 --- a/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerConfiguration.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2000-2009 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.intellij.spellchecker.options; - -import com.intellij.openapi.components.PersistentStateComponent; -import com.intellij.openapi.components.State; -import com.intellij.openapi.components.Storage; -import com.intellij.openapi.components.StorageScheme; -import com.intellij.util.xmlb.XmlSerializerUtil; - -@State( - name = "spellchecker-configuration", - storages = { - @Storage( - id = "other", - file = "$PROJECT_FILE$"), - @Storage( - id = "dir", - file = "$PROJECT_CONFIG_DIR$/spellchecker.xml", - scheme = StorageScheme.DIRECTORY_BASED)}) -public final class SpellCheckerConfiguration implements PersistentStateComponent { - - - public SpellCheckerConfiguration getState() { - return this; - } - - public void loadState(SpellCheckerConfiguration state) { - XmlSerializerUtil.copyBean(state, this); - } - -} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerOptions.form b/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerOptions.form index 27582a3ad7a3..cb28418ead23 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerOptions.form +++ b/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerOptions.form @@ -1,70 +1,46 @@ - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerOptions.java b/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerOptions.java index b30b356a6eb0..a5412b93e260 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerOptions.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/options/SpellCheckerOptions.java @@ -1,187 +1,169 @@ -/* - * Copyright 2000-2009 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.intellij.spellchecker.options; - -import com.intellij.openapi.Disposable; -import com.intellij.openapi.ui.Messages; -import com.intellij.spellchecker.dictionary.Dictionary; -import com.intellij.spellchecker.SpellCheckerManager; -import com.intellij.spellchecker.util.SpellCheckerBundle; -import com.intellij.spellchecker.util.Strings; -import com.intellij.ui.AddDeleteListPanel; -import com.intellij.util.containers.HashSet; - -import javax.swing.*; -import javax.swing.event.ChangeListener; -import javax.swing.event.ChangeEvent; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; - - -public class SpellCheckerOptions implements Disposable { - - private final SpellCheckerConfiguration configuration; - private final SpellCheckerManager manager; - - private JPanel root; - - private WordsPanel userDictionaryWords; - /*private WordsPanel ignoredWords;*/ - private JRadioButton projectRB; - private JRadioButton localRB; - private JLabel globalDictionaries; - - private Dictionary shownDictionary; - - public Dictionary getShownDictionary() { - return shownDictionary; - } - - public SpellCheckerOptions(SpellCheckerConfiguration configuration, SpellCheckerManager manager) { - this.configuration = configuration; - this.manager = manager; - } - - private void createUIComponents() { - userDictionaryWords = new WordsPanel(manager.getProjectWordList().getWords(), manager); - /*ignoredWords = new WordsPanel(manager.getProjectWordList().getIgnoredWords(), manager);*/ - shownDictionary = manager.getProjectWordList(); - - } - - public Set getUserDictionaryWordsSet() { - return getWords(userDictionaryWords); - } - - public boolean useProjectDictionary() { - return projectRB.isSelected(); - } - - public void setUserDictionaryWords(Set words) { - userDictionaryWords.replaceAll(words); - } - - /*public Set getIgnoredWords() { - return getWords(ignoredWords); - }*/ - - /* public void setIgnoredWords(Set dictionary) { - ignoredWords.replaceAll(dictionary); - }*/ - - - public JPanel getRoot() { - projectRB.setSelected(true); - projectRB.addChangeListener(new ChangeListener() { - public void stateChanged(ChangeEvent e) { - if (projectRB.isSelected()) { - shownDictionary = manager.getProjectWordList(); - userDictionaryWords.replaceAll(shownDictionary.getWords()); - /*ignoredWords.replaceAll(shownDictionary.getIgnoredWords());*/ - } - } - }); - localRB.addChangeListener(new ChangeListener() { - public void stateChanged(ChangeEvent e) { - if (localRB.isSelected()) { - shownDictionary = manager.getCachedWordList(); - userDictionaryWords.replaceAll(shownDictionary.getWords()); - /*ignoredWords.replaceAll(shownDictionary.getIgnoredWords());*/ - } - } - }); - - if (manager.getDictionaries() != null) { - String label = "Global dictionaries: "; - for (String dic : manager.getDictionaries()) { - label += dic + "; "; - } - globalDictionaries.setText(label); - } - return root; - } - - - private static Set getWords(AddDeleteListPanel panel) { - Set words = new HashSet(); - Object[] objects = panel.getListItems(); - for (Object object : objects) { - words.add((String)object); - } - return words; - } - - - public void dispose() { - userDictionaryWords.dispose(); - /*ignoredWords.dispose();*/ - } - - - private static final class WordsPanel extends AddDeleteListPanel implements Disposable { - private SpellCheckerManager manager; - - private WordsPanel(Set words, SpellCheckerManager manager) { - super(null, sort(words)); - this.manager = manager; - } - - private static List sort(Set words) { - List arrayList = new ArrayList(words); - Collections.sort(arrayList); - return arrayList; - } - - - - protected Object findItemToAdd() { - String word = - Messages.showInputDialog(SpellCheckerBundle.message("enter.simple.word"), SpellCheckerBundle.message("add.new.word"), null); - if (word == null) { - return null; - } - else { - word = word.trim(); - } - - if (Strings.isMixedCase(word)) { - Messages.showWarningDialog(SpellCheckerBundle.message("entered.word.0.is.mixed.cased.you.must.enter.simple.word", word), - SpellCheckerBundle.message("add.new.word")); - return null; - } - if (!manager.hasProblem(word)) { - Messages.showWarningDialog(SpellCheckerBundle.message("entered.word.0.is.correct.you.no.need.to.add.this.in.list", word), - SpellCheckerBundle.message("add.new.word")); - return null; - } - return word; - } - - public void replaceAll(Set words) { - myList.clearSelection(); - myListModel.removeAllElements(); - for (String word : sort(words)) { - myListModel.addElement(word); - } - } - - public void dispose() { - myListModel.removeAllElements(); - } - } -} +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.options; + + +import com.intellij.openapi.Disposable; +import com.intellij.openapi.ui.Messages; +import com.intellij.spellchecker.SpellCheckerManager; +import com.intellij.spellchecker.dictionary.Dictionary; +import com.intellij.spellchecker.util.SpellCheckerBundle; +import com.intellij.spellchecker.util.Strings; +import com.intellij.ui.AddDeleteListPanel; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + + +public class SpellCheckerOptions implements Disposable { + + private final SpellCheckerManager manager; + private JPanel root; + private JPanel wordPanelHolder; + private WordsPanel wordsPanel; + + + + public SpellCheckerOptions(SpellCheckerManager manager) { + this.manager = manager; + } + + public void createUIComponents() { + wordsPanel = new WordsPanel(manager); + + } + + public JPanel getRoot() { + return root; + } + + @Nullable + public List getWords(){ + Object[] pairs = wordsPanel.getListItems(); + if (pairs==null){ + return null; + } + List words = new ArrayList(); + for (Object pair : pairs) { + words.add(pair.toString()); + } + return words; + } + + public void dispose() { + + wordsPanel.dispose(); + } + + + public static final class WordDescriber { + private Dictionary dictionary; + + public WordDescriber(Dictionary dictionary) { + this.dictionary = dictionary; + } + + @NotNull + public List process() { + if (this.dictionary == null) { + return new ArrayList(); + } + Set words = this.dictionary.getEditableWords(); + if (words == null) { + return new ArrayList(); + } + List result = new ArrayList(); + for (String word : words) { + result.add(new Pair(word, "")); + } + Collections.sort(result); + return result; + } + } + + public static final class Pair implements Comparable { + private String word; + private String description; + + public Pair(@NotNull String word, String description) { + this.word = word; + this.description = description; + } + + public String getWord() { + return word; + } + + public String getDescription() { + return description; + } + + public int compareTo(Object o) { + if (!(o instanceof Pair)) { + throw new IllegalArgumentException(); + } + return word.compareTo(((Pair)o).getWord()); + } + + @Override + public String toString() { + return word + (description!=null && description.trim().length()>0?"("+description+")":""); + } + } + + private static final class WordsPanel extends AddDeleteListPanel implements Disposable { + private SpellCheckerManager manager; + + private WordsPanel(SpellCheckerManager manager) { + super(null, new WordDescriber(manager.getUserDictionary()).process()); + this.manager = manager; + } + + + protected Object findItemToAdd() { + String word = Messages.showInputDialog(com.intellij.spellchecker.util.SpellCheckerBundle.message("enter.simple.word"), + SpellCheckerBundle.message("add.new.word"), null); + if (word == null) { + return null; + } + else { + word = word.trim(); + } + + if (Strings.isMixedCase(word)) { + Messages.showWarningDialog(SpellCheckerBundle.message("entered.word.0.is.mixed.cased.you.must.enter.simple.word", word), + SpellCheckerBundle.message("add.new.word")); + return null; + } + if (!manager.hasProblem(word)) { + Messages.showWarningDialog(SpellCheckerBundle.message("entered.word.0.is.correct.you.no.need.to.add.this.in.list", word), + SpellCheckerBundle.message("add.new.word")); + return null; + } + return word; + } + + + public void dispose() { + myListModel.removeAllElements(); + } + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/AcceptWordAsCorrect.java b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/AcceptWordAsCorrect.java index 499eb8351628..18a93ed00d80 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/AcceptWordAsCorrect.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/AcceptWordAsCorrect.java @@ -19,7 +19,7 @@ import com.intellij.codeInspection.ProblemDescriptor; import com.intellij.openapi.actionSystem.Anchor; import com.intellij.openapi.project.Project; import com.intellij.spellchecker.SpellCheckerManager; -import com.intellij.spellchecker.quickfixes.SpellCheckerQuickFix; + import com.intellij.spellchecker.util.SpellCheckerBundle; import org.jetbrains.annotations.NotNull; @@ -50,4 +50,5 @@ public class AcceptWordAsCorrect implements SpellCheckerQuickFix { SpellCheckerManager spellCheckerManager = SpellCheckerManager.getInstance(project); spellCheckerManager.acceptWordAsCorrect(word); } + } diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/ChangeTo.java b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/ChangeTo.java index 407e24c57e58..b970c217e3d8 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/ChangeTo.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/ChangeTo.java @@ -26,25 +26,19 @@ import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; import com.intellij.openapi.util.TextRange; import com.intellij.openapi.util.text.StringUtil; -import com.intellij.spellchecker.SpellCheckerManager; import com.intellij.spellchecker.util.SpellCheckerBundle; import org.jetbrains.annotations.NotNull; import java.util.ArrayList; import java.util.List; -public class ChangeTo implements SpellCheckerQuickFix { - - private TextRange textRange; - private String word; - private Project project; +public class ChangeTo extends ShowSuggestions implements SpellCheckerQuickFix { public ChangeTo(@NotNull TextRange textRange, @NotNull String word, @NotNull Project project) { - this.textRange = textRange; - this.word = word; - this.project = project; + super(textRange, word, project); } + @NotNull public String getName() { return SpellCheckerBundle.message("change.to"); @@ -60,7 +54,7 @@ public class ChangeTo implements SpellCheckerQuickFix { return Anchor.FIRST; } - + public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) { final Editor editor = PlatformDataKeys.EDITOR.getData(DataManager.getInstance().getDataContext()); @@ -78,11 +72,8 @@ public class ChangeTo implements SpellCheckerQuickFix { return; } - SpellCheckerManager manager = SpellCheckerManager.getInstance(project); - List variants = manager.getSuggestions(word); - List lookupItems = new ArrayList(); - for (String variant : variants) { + for (String variant : getSuggestions()) { lookupItems.add(LookupElementBuilder.create(variant)); } LookupElement[] items = new LookupElement[lookupItems.size()]; @@ -90,7 +81,6 @@ public class ChangeTo implements SpellCheckerQuickFix { LookupManager lookupManager = LookupManager.getInstance(project); lookupManager.showLookup(editor, items); - } diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/DictionarySuggestionProvider.java b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/DictionarySuggestionProvider.java similarity index 96% rename from plugins/spellchecker/src/com/intellij/spellchecker/DictionarySuggestionProvider.java rename to plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/DictionarySuggestionProvider.java index 2f6288e6a88d..2e9ea068cae2 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/DictionarySuggestionProvider.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/DictionarySuggestionProvider.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.intellij.spellchecker; +package com.intellij.spellchecker.quickfixes; import com.intellij.codeInsight.lookup.LookupElement; import com.intellij.psi.PsiElement; @@ -21,6 +21,7 @@ import com.intellij.psi.PsiNamedElement; import com.intellij.psi.codeStyle.NameUtil; import com.intellij.psi.codeStyle.SuggestedNameInfo; import com.intellij.refactoring.rename.NameSuggestionProvider; +import com.intellij.spellchecker.SpellCheckerManager; import java.util.*; diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/RenameTo.java b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/RenameTo.java index e2264a1f2a0c..d9385e0fbc35 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/RenameTo.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/RenameTo.java @@ -20,10 +20,10 @@ import com.intellij.ide.DataManager; import com.intellij.openapi.actionSystem.*; import com.intellij.openapi.extensions.Extensions; import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; import com.intellij.refactoring.actions.RenameElementAction; import com.intellij.refactoring.rename.NameSuggestionProvider; -import com.intellij.spellchecker.DictionarySuggestionProvider; -import com.intellij.spellchecker.quickfixes.SpellCheckerQuickFix; +import com.intellij.spellchecker.quickfixes.DictionarySuggestionProvider; import com.intellij.spellchecker.util.SpellCheckerBundle; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -31,7 +31,11 @@ import org.jetbrains.annotations.Nullable; import javax.swing.*; -public class RenameTo implements SpellCheckerQuickFix { +public class RenameTo extends ShowSuggestions implements SpellCheckerQuickFix { + + public RenameTo(@NotNull TextRange textRange, @NotNull String word, @NotNull Project project) { + super(textRange, word, project); + } @NotNull public String getName() { diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/ShowSuggestions.java b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/ShowSuggestions.java new file mode 100644 index 000000000000..d41972672461 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/ShowSuggestions.java @@ -0,0 +1,58 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.quickfixes; + +import com.intellij.codeInspection.LocalQuickFix; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.util.TextRange; +import com.intellij.spellchecker.SpellCheckerManager; +import org.jetbrains.annotations.NotNull; + +import java.util.List; + + +public abstract class ShowSuggestions implements LocalQuickFix { + + protected TextRange textRange; + protected String word; + protected Project project; + private List suggestions; + private boolean processed; + + + + + public ShowSuggestions(@NotNull TextRange textRange, @NotNull String word, @NotNull Project project) { + this.textRange = textRange; + this.word = word; + this.project = project; + } + + @NotNull + public List getSuggestions(){ + if (!processed){ + calculateSuggestions(); + processed=true; + } + return suggestions; + } + + private void calculateSuggestions(){ + SpellCheckerManager manager = SpellCheckerManager.getInstance(project); + suggestions = manager.getSuggestions(word); + } + +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/SpellCheckerQuickFix.java b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/SpellCheckerQuickFix.java index 94fd0b8cbc64..9bb123f549fd 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/SpellCheckerQuickFix.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/quickfixes/SpellCheckerQuickFix.java @@ -19,16 +19,8 @@ import com.intellij.codeInspection.LocalQuickFix; import com.intellij.openapi.actionSystem.Anchor; import org.jetbrains.annotations.NotNull; -/** - * Spell checker quick fix. - */ public interface SpellCheckerQuickFix extends LocalQuickFix { - /** - * Return anchor for actions. Basically return {@link com.intellij.openapi.actionSystem.Anchor#FIRST} for common - * suggest words, and {@link com.intellij.openapi.actionSystem.Anchor#LAST} for 'Add to ...' actions. - * - * @return Action anchor - */ + @NotNull Anchor getPopupActionAnchor(); } diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/state/AggregatedDictionaryState.java b/plugins/spellchecker/src/com/intellij/spellchecker/state/AggregatedDictionaryState.java new file mode 100644 index 000000000000..c2fac874be88 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/state/AggregatedDictionaryState.java @@ -0,0 +1,84 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.state; + +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.project.Project; +import com.intellij.spellchecker.dictionary.AggregatedDictionary; +import com.intellij.spellchecker.dictionary.ProjectDictionary; +import com.intellij.spellchecker.dictionary.UserDictionary; +import org.jetbrains.annotations.NotNull; + +public class AggregatedDictionaryState { + + private ProjectDictionaryState projectDictionaryState; + private CachedDictionaryState cachedDictionaryState; + private AggregatedDictionary dictionary; + private String currentUser; + private Project project; + + + public AggregatedDictionaryState() { + } + + public AggregatedDictionaryState(@NotNull AggregatedDictionary dictionary) { + setDictionary(dictionary); + } + + public void setProject(Project project) { + this.project = project; + } + + public void setCurrentUser(String currentUser) { + this.currentUser = currentUser; + } + + public void setDictionary(AggregatedDictionary dictionary) { + this.dictionary = dictionary; + cachedDictionaryState.setDictionary(dictionary.getCachedDictionary()); + projectDictionaryState.setProjectDictionary(dictionary.getProjectDictionary()); + } + + public AggregatedDictionary getDictionary() { + return dictionary; + } + + + + public void loadState() { + assert project != null; + cachedDictionaryState = ServiceManager.getService(project, CachedDictionaryState.class); + projectDictionaryState = ServiceManager.getService(project, ProjectDictionaryState.class); + currentUser = System.getProperty("user.name"); + retrieveDictionaries(); + } + + private void retrieveDictionaries() { + ProjectDictionary projectDictionary = projectDictionaryState.getProjectDictionary(); + projectDictionary.setActiveName(currentUser); + + if (cachedDictionaryState.getDictionary() == null) { + cachedDictionaryState.setDictionary(new UserDictionary(CachedDictionaryState.DEFAULT_NAME)); + } + dictionary = new AggregatedDictionary(projectDictionary, cachedDictionaryState.getDictionary()); + setDictionary(dictionary); + } + + @Override + public String toString() { + return "AggregatedDictionaryState{" + "dictionary=" + dictionary + '}'; + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/options/CachedDictionaryState.java b/plugins/spellchecker/src/com/intellij/spellchecker/state/CachedDictionaryState.java similarity index 62% rename from plugins/spellchecker/src/com/intellij/spellchecker/options/CachedDictionaryState.java rename to plugins/spellchecker/src/com/intellij/spellchecker/state/CachedDictionaryState.java index 5b3be8ebb8a9..0cc66939a1d9 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/options/CachedDictionaryState.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/state/CachedDictionaryState.java @@ -1,45 +1,47 @@ -/* - * Copyright 2000-2009 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.intellij.spellchecker.options; - -import com.intellij.openapi.components.PersistentStateComponent; -import com.intellij.openapi.components.State; -import com.intellij.openapi.components.Storage; -import com.intellij.util.xmlb.XmlSerializerUtil; -import com.intellij.spellchecker.dictionary.UserDictionary; -import com.intellij.spellchecker.dictionary.Dictionary; - -@State( - name = "CachedDictionaryState", - storages = {@Storage( - id = "spellchecker", - file = "$APP_CONFIG$/cachedDictionary.xml")}) -public class CachedDictionaryState implements PersistentStateComponent { - - public UserDictionary dictionary = new UserDictionary("cached"); - - public CachedDictionaryState getState() { - return this; - } - - public void loadState(CachedDictionaryState state) { - XmlSerializerUtil.copyBean(state, this); - } - - public Dictionary getDictionary(){ - return dictionary; - } -} +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.state; + +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; +import com.intellij.spellchecker.dictionary.Dictionary; + +@State( + name = "CachedDictionaryState", + storages = {@Storage( + id = "spellchecker", + file = "$APP_CONFIG$/cachedDictionary.xml")}) +public class CachedDictionaryState extends DictionaryState implements PersistentStateComponent{ + public static final String DEFAULT_NAME = "cached"; + + public CachedDictionaryState() { + name = DEFAULT_NAME; + } + + public CachedDictionaryState(Dictionary dictionary) { + super(dictionary); + name = DEFAULT_NAME; + } + + @Override + public void loadState(DictionaryState state) { + if (state.name==null){ + state.name= DEFAULT_NAME; + } + super.loadState(state); + } +} \ No newline at end of file diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/state/DictionaryState.java b/plugins/spellchecker/src/com/intellij/spellchecker/state/DictionaryState.java new file mode 100644 index 000000000000..857e7069f990 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/state/DictionaryState.java @@ -0,0 +1,97 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.state; + +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.spellchecker.dictionary.Dictionary; +import com.intellij.spellchecker.dictionary.UserDictionary; +import com.intellij.util.xmlb.annotations.AbstractCollection; +import com.intellij.util.xmlb.annotations.Attribute; +import com.intellij.util.xmlb.annotations.Tag; +import com.intellij.util.xmlb.annotations.Transient; +import org.jetbrains.annotations.NotNull; + +import java.util.HashSet; +import java.util.Set; + +@Tag("dictionary") +public class DictionaryState implements PersistentStateComponent { + + public static final String NAME_ATTRIBUTE = "name"; + + @Tag("words") @AbstractCollection(surroundWithTag = false, elementTag = "w", elementValueAttribute = "") + public Set words = new HashSet(); + + @Attribute(NAME_ATTRIBUTE) + public String name; + + @Transient + private Dictionary dictionary; + + public DictionaryState() { + } + + public DictionaryState(@NotNull Dictionary dictionary) { + setDictionary(dictionary); + } + + @Transient + public void setDictionary(@NotNull Dictionary dictionary) { + this.dictionary = dictionary; + this.name = dictionary.getName(); + synchronizeWords(); + } + + @Transient + public Dictionary getDictionary() { + return dictionary; + } + + public DictionaryState getState() { + synchronizeWords(); + return this; + } + + private void synchronizeWords() { + if (dictionary != null) { + Set words = new HashSet(); + words.addAll(dictionary.getWords()); + this.words = words; + } + } + + public void loadState(DictionaryState state) { + if (state != null && state.name != null) { + name = state.name; + words = state.words; + } + retrieveDictionary(); + } + + private void retrieveDictionary() { + assert name != null; + dictionary = new UserDictionary(name); + dictionary.addToDictionary(words); + } + + + @Override + public String toString() { + return "DictionaryState{" + "dictionary=" + dictionary + '}'; + } + + +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/options/ProjectDictionarySplitter.java b/plugins/spellchecker/src/com/intellij/spellchecker/state/ProjectDictionarySplitter.java similarity index 86% rename from plugins/spellchecker/src/com/intellij/spellchecker/options/ProjectDictionarySplitter.java rename to plugins/spellchecker/src/com/intellij/spellchecker/state/ProjectDictionarySplitter.java index a0d1aeff7c74..dd2a5ff8d724 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/options/ProjectDictionarySplitter.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/state/ProjectDictionarySplitter.java @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.intellij.spellchecker.options; +package com.intellij.spellchecker.state; import com.intellij.openapi.components.StateSplitter; -import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.JDOMUtil; +import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.io.FileUtil; +import com.intellij.spellchecker.state.DictionaryState; import com.intellij.util.text.UniqueNameGenerator; import org.jdom.Element; @@ -36,8 +37,9 @@ public class ProjectDictionarySplitter implements StateSplitter { final UniqueNameGenerator generator = new UniqueNameGenerator(); List> result = new ArrayList>(); - for (Element element : JDOMUtil.getElements(e)) { - final String name = generator.generateUniqueName(FileUtil.sanitizeFileName(element.getAttributeValue("name"))) + ".xml"; + for (Element element : JDOMUtil.getElements(e)) { + final String name = generator.generateUniqueName(FileUtil.sanitizeFileName(element.getAttributeValue(DictionaryState.NAME_ATTRIBUTE))) + ".xml"; + result.add(new Pair(element, name)); } return result; diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/state/ProjectDictionaryState.java b/plugins/spellchecker/src/com/intellij/spellchecker/state/ProjectDictionaryState.java new file mode 100644 index 000000000000..97e996887049 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/state/ProjectDictionaryState.java @@ -0,0 +1,118 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.state; + +import com.intellij.openapi.components.PersistentStateComponent; +import com.intellij.openapi.components.State; +import com.intellij.openapi.components.Storage; +import com.intellij.openapi.components.StorageScheme; +import com.intellij.openapi.project.Project; +import com.intellij.spellchecker.dictionary.Dictionary; +import com.intellij.spellchecker.dictionary.ProjectDictionary; +import com.intellij.spellchecker.state.ProjectDictionarySplitter; +import com.intellij.util.containers.hash.HashSet; +import com.intellij.util.xmlb.annotations.AbstractCollection; +import com.intellij.util.xmlb.annotations.Property; +import com.intellij.util.xmlb.annotations.Transient; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +@State( + name = "ProjectDictionaryState", + storages = {@Storage( + id = "other", + file = "$PROJECT_FILE$"), + @Storage( + id = "dir", + file = "$PROJECT_CONFIG_DIR$/dictionaries/", + scheme = StorageScheme.DIRECTORY_BASED, stateSplitter = ProjectDictionarySplitter.class)}) + +public class ProjectDictionaryState implements PersistentStateComponent{ + + @Property(surroundWithTag = false) @AbstractCollection(surroundWithTag = false, elementTypes = DictionaryState.class) + public List dictionaryStates = new ArrayList(); + + + private ProjectDictionary projectDictionary; + private String currentUser; + private Project project; + + public ProjectDictionaryState() { + } + + public void setProject(Project project) { + this.project = project; + } + + public void setCurrentUser(String currentUser) { + this.currentUser = currentUser; + } + + + @Transient + public void setProjectDictionary(ProjectDictionary projectDictionary) { + currentUser = projectDictionary.getActiveName(); + dictionaryStates.clear(); + Set projectDictionaries = projectDictionary.getDictionaries(); + if (projectDictionaries != null) { + for (Dictionary dic : projectDictionary.getDictionaries()) { + dictionaryStates.add(new DictionaryState(dic)); + } + } + } + + @Transient + public ProjectDictionary getProjectDictionary() { + if (projectDictionary==null){ + projectDictionary = new ProjectDictionary(); + } + return projectDictionary; + } + + public ProjectDictionaryState getState() { + if (projectDictionary!=null){ + //ensure all dictionaries within project dictionary will be stored + setProjectDictionary(projectDictionary); + } + return this; + } + + + public void loadState(ProjectDictionaryState state) { + if (state != null) { + this.dictionaryStates = state.dictionaryStates; + } + retrieveProjectDictionaries(); + } + + private void retrieveProjectDictionaries() { + Set dictionaries = new HashSet(); + if (dictionaryStates != null) { + for (DictionaryState dictionaryState : dictionaryStates) { + dictionaryState.loadState(dictionaryState); + dictionaries.add(dictionaryState.getDictionary()); + } + } + projectDictionary = new ProjectDictionary(dictionaries); + } + + @Override + public String toString() { + return "ProjectDictionaryState{" + "projectDictionary=" + projectDictionary + '}'; + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/state/StateLoader.java b/plugins/spellchecker/src/com/intellij/spellchecker/state/StateLoader.java new file mode 100644 index 000000000000..7a9f9292012f --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/state/StateLoader.java @@ -0,0 +1,56 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.state; + +import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.project.Project; +import com.intellij.spellchecker.dictionary.Dictionary; +import com.intellij.spellchecker.dictionary.Loader; +import com.intellij.spellchecker.dictionary.Processor; +import org.jetbrains.annotations.NotNull; + +import java.util.Set; + + +public class StateLoader implements Loader { + + private Project project; + private Dictionary dictionary; + + public StateLoader(Project project) { + this.project = project; + } + + + public void load(@NotNull Processor processor) { + AggregatedDictionaryState state = ServiceManager.getService(project, AggregatedDictionaryState.class); + state.setProject(project); + state.loadState(); + dictionary = state.getDictionary(); + final Set storedWords = dictionary.getWords(); + if (storedWords!=null){ + for (String word : storedWords) { + processor.process(word); + } + } + } + + public Dictionary getDictionary() { + return dictionary; + } +} + + diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/trie/Action.java b/plugins/spellchecker/src/com/intellij/spellchecker/trie/Action.java new file mode 100644 index 000000000000..4391ff8ca6e2 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/trie/Action.java @@ -0,0 +1,23 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.trie; + +import java.util.Map; + +public interface Action { + + void run(Map.Entry entry); +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/trie/CharSequenceKeyAnalyzer.java b/plugins/spellchecker/src/com/intellij/spellchecker/trie/CharSequenceKeyAnalyzer.java new file mode 100644 index 000000000000..a549b936f43c --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/trie/CharSequenceKeyAnalyzer.java @@ -0,0 +1,144 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.trie; + + + +/** + * Analyzes CharSequence keys with case sensitivity. With + * CharSequenceKeyAnalyzer you can + * compare, check prefix, and determine the index of a bit. + *

+ * A typical use case for a CharSequenceKeyAnalyzer is with a + * {@link PatriciaTrie}. + *

+    PatriciaTrie<String, String> trie = new PatriciaTrie<String, String>(new CharSequenceKeyAnalyzer());
+
+    trie.put("Lime", "Lime");
+    trie.put("LimeWire", "LimeWire");
+    trie.put("LimeRadio", "LimeRadio");
+    trie.put("Lax", "Lax");
+    trie.put("Lake", "Lake");
+    trie.put("Lovely", "Lovely");
+
+    System.out.println(trie.select("Lo"));
+    System.out.println(trie.select("Lime"));
+
+    System.out.println(trie.getPrefixedBy("La").toString());
+
+    Output:
+        Lovely
+        Lime
+        {Lake=Lake, Lax=Lax}
+
+ * 
+ * + * @author Sam Berlin + * @author Roger Kapsi + */ +public class CharSequenceKeyAnalyzer implements PatriciaTrie.KeyAnalyzer { + + private static final long serialVersionUID = -7032449491269434877L; + + private static final int[] BITS = createIntBitMask(16); + + public static final int[] createIntBitMask(int bitCount) { + int[] bits = new int[bitCount]; + for(int i = 0; i < bitCount; i++) { + bits[i] = 1 << (bitCount - i - 1); + } + return bits; + } + public int length(CharSequence key) { + return (key != null ? key.length() * 16 : 0); + } + + public int bitIndex(CharSequence key, int keyOff, int keyLength, + CharSequence found, int foundOff, int foundKeyLength) { + boolean allNull = true; + + if(keyOff % 16 != 0 || foundOff % 16 != 0 || + keyLength % 16 != 0 || foundKeyLength % 16 != 0) + throw new IllegalArgumentException("offsets & lengths must be at character boundaries"); + + int off1 = keyOff / 16; + int off2 = foundOff / 16; + int len1 = keyLength / 16 + off1; + int len2 = foundKeyLength / 16 + off2; + int length = Math.max(len1, len2); + + // Look at each character, and if they're different + // then figure out which bit makes the difference + // and return it. + char k = 0, f = 0; + for(int i = 0; i < length; i++) { + int kOff = i + off1; + int fOff = i + off2; + + if(kOff >= len1) + k = 0; + else + k = key.charAt(kOff); + + if(found == null || fOff >= len2) + f = 0; + else + f = found.charAt(fOff); + + if(k != f) { + int x = k ^ f; + return i * 16 + (Integer.numberOfLeadingZeros(x) - 16); + } + + if(k != 0) + allNull = false; + + } + + if (allNull) { + return PatriciaTrie.KeyAnalyzer.NULL_BIT_KEY; + } + + return PatriciaTrie.KeyAnalyzer.EQUAL_BIT_KEY; + } + + public boolean isBitSet(CharSequence key, int keyLength, int bitIndex) { + if (key == null || bitIndex >= keyLength) { + return false; + } + + int index = bitIndex / BITS.length; + int bit = bitIndex - index * BITS.length; + return (key.charAt(index) & BITS[bit]) != 0; + } + + public int compare(CharSequence o1, CharSequence o2) { + return o1.toString().compareTo(o2.toString()); + } + + public int bitsPerElement() { + return 16; + } + + public boolean isPrefix(CharSequence prefix, int offset, int length, CharSequence key) { + if(offset % 16 != 0 || length % 16 != 0) + throw new IllegalArgumentException("Cannot determine prefix outside of character boundaries"); + String s1 = prefix.subSequence(offset / 16, length / 16).toString(); + String s2 = key.toString(); + return s2.startsWith(s1); + } +} + diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/trie/PatriciaTrie.java b/plugins/spellchecker/src/com/intellij/spellchecker/trie/PatriciaTrie.java new file mode 100644 index 000000000000..9770c9d7aba1 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/trie/PatriciaTrie.java @@ -0,0 +1,2254 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.trie; + + +import java.io.Serializable; +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Comparator; +import java.util.ConcurrentModificationException; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.SortedMap; + +/** + * A PATRICIA Trie. + *

+ * PATRICIA = Practical Algorithm to Retrieve Information Coded in Alphanumeric + *

+ * A PATRICIA Trie is a compressed Trie. Instead of storing all data at the + * edges of the Trie (and having empty internal nodes), PATRICIA stores data + * in every node. This allows for very efficient traversal, insert, delete, + * predecessor, successor, prefix, range, and 'select' operations. All operations + * are performed at worst in O(K) time, where K is the number of bits in the + * largest item in the tree. In practice, operations actually take O(A(K)) + * time, where A(K) is the average number of bits of all items in the tree. + *

+ * Most importantly, PATRICIA requires very few comparisons to keys while + * doing any operation. While performing a lookup, each comparison + * (at most K of them, described above) will perform a single bit comparison + * against the given key, instead of comparing the entire key to another key. + *

+ * The Trie can return operations in lexicographical order using the 'traverse', + * 'prefix', 'submap', or 'iterator' methods. The Trie can also scan for items + * that are 'bitwise' (using an XOR metric) by the 'select' method. Bitwise + * closeness is determined by the {@link KeyAnalyzer} returning true or + * false for a bit being set or not in a given key. + *

+ * This PATRICIA Trie supports both variable length & fixed length keys. + * Some methods, such as getPrefixedBy(...) are suited only to + * variable length keys, whereas getPrefixedByBits(...) is suited + * to fixed-size keys. + *

+ * Additionally see PATRICIA + * for more information. + *

+ * Any methods here that take an Object may throw a + * ClassCastException if the method is expecting an instance of K + * (and it isn't K). + * + *

+    PatriciaTrie<String, String> trie = new PatriciaTrie<String, String>
+    (new CharSequenceKeyAnalyzer());
+
+    trie.put("Lime", "Lime");
+    trie.put("LimeWire", "LimeWire");
+    trie.put("LimeRadio", "LimeRadio");
+    trie.put("Lax", "Lax");
+    trie.put("Lake", "Lake");
+    trie.put("Lovely", "Lovely");
+
+    System.out.println(trie.select("Lo"));
+    System.out.println(trie.select("Lime"));
+
+    System.out.println(trie.getPrefixedBy("La").toString());
+
+    Output:
+        Lovely
+        Lime
+        {Lake=Lake, Lax=Lax}
+
+ * 
+ * @author Roger Kapsi + * @author Sam Berlin + + */ +public class PatriciaTrie extends AbstractMap implements Trie, Serializable { + + private static final long serialVersionUID = 110232526181493307L; + + /** The root element of the Trie. */ + private final TrieEntry root = new TrieEntry(null, null, -1); + + /** The current size (total number of elements) of the Trie. */ + private int size = 0; + + /** The number of times this has been modified (to fail-fast the iterators). */ + private transient int modCount = 0; + + /** The keyAnalyzer used to analyze bit values of keys. */ + private final KeyAnalyzer keyAnalyzer; + + /** Constructs a new PatriciaTrie using the given keyAnalyzer. */ + public PatriciaTrie(KeyAnalyzer keyAnalyzer) { + this.keyAnalyzer = keyAnalyzer; + } + + /** Returns the KeyAnalyzer that constructed the trie. */ + public KeyAnalyzer getKeyAnalyzer() { + return keyAnalyzer; + } + + /** Returns the KeyAnalyzer as a comparator. */ + public Comparator comparator() { + return keyAnalyzer; + } + + /** Clears the Trie (i.e. removes all elements). */ + public void clear() { + root.key = null; + root.bitIndex = -1; + root.value = null; + + root.parent = null; + root.left = root; + root.right = null; + root.predecessor = root; + + size = 0; + incrementModCount(); + } + + /** Returns true if the Trie is empty */ + public boolean isEmpty() { + return size == 0; + } + + /** Returns the number items in the Trie */ + public int size() { + return size; + } + + /** Increments both the size and mod counter. */ + private void incrementSize() { + size++; + incrementModCount(); + } + + /** Decrements the size and increments the mod counter. */ + private void decrementSize() { + size--; + incrementModCount(); + } + + /** Increments the mod counter. */ + private void incrementModCount() { + modCount++; + } + + /** + * Adds a new pair to the Trie and if a pair already + * exists it will be replaced. In the latter case it will return + * the old value. + */ + public V put(K key, V value) { + if (key == null) { + throw new NullPointerException("Key cannot be null"); + } + + int keyLength = length(key); + + // The only place to store a key with a length + // of zero bits is the root node + if (keyLength == 0) { + if (root.isEmpty()) { + incrementSize(); + } else { + incrementModCount(); + } + return root.setKeyValue(key, value); + } + + TrieEntry found = getNearestEntryForKey(key, keyLength); + if (key.equals(found.key)) { + if (found.isEmpty()) { // <- must be the root + incrementSize(); + } else { + incrementModCount(); + } + return found.setKeyValue(key, value); + } + + int bitIndex = bitIndex(key, found.key); + if (isValidBitIndex(bitIndex)) { // in 99.999...9% the case + /* NEW KEY+VALUE TUPLE */ + TrieEntry t = new TrieEntry(key, value, bitIndex); + addEntry(t, keyLength); + incrementSize(); + return null; + } else if (isNullBitKey(bitIndex)) { + // A bits of the Key are zero. The only place to + // store such a Key is the root Node! + + /* NULL BIT KEY */ + if (root.isEmpty()) { + incrementSize(); + } else { + incrementModCount(); + } + return root.setKeyValue(key, value); + + } else if (isEqualBitKey(bitIndex)) { + // This is a very special and rare case. + + /* REPLACE OLD KEY+VALUE */ + if (found != root) { + incrementModCount(); + return found.setKeyValue(key, value); + } + } + + throw new IndexOutOfBoundsException("Failed to put: " + key + " -> " + value + ", " + bitIndex); + } + + /** Adds the given entry into the Trie. */ + private TrieEntry addEntry(TrieEntry toAdd, int keyLength) { + TrieEntry current = root.left; + TrieEntry path = root; + while(true) { + if(current.bitIndex >= toAdd.bitIndex || current.bitIndex <= path.bitIndex) { + toAdd.predecessor = toAdd; + + if (!isBitSet(toAdd.key, keyLength, toAdd.bitIndex)) { + toAdd.left = toAdd; + toAdd.right = current; + } else { + toAdd.left = current; + toAdd.right = toAdd; + } + + toAdd.parent = path; + if (current.bitIndex >= toAdd.bitIndex) { + current.parent = toAdd; + } + + // if we inserted an uplink, set the predecessor on it + if(current.bitIndex <= path.bitIndex) { + current.predecessor = toAdd; + } + + if (path == root || !isBitSet(toAdd.key, keyLength, path.bitIndex)) + path.left = toAdd; + else + path.right = toAdd; + return toAdd; + } + + path = current; + if(!isBitSet(toAdd.key, keyLength, current.bitIndex)) + current = current.left; + else + current = current.right; + } + } + + @Override + public Set> entrySet() { + Set> es = entrySet; + return (es != null ? es : (entrySet = new EntrySet())); + } + + /** + * Returns the Value whose Key equals our lookup Key + * or null if no such key exists. + */ + public V get(Object k) { + TrieEntry entry = getEntry(k); + return entry != null ? entry.getValue() : null; + } + + /** + * Returns the entry associated with the specified key in the + * PatriciaTrie. Returns null if the map contains no mapping + * for this key. + * + * This may throw ClassCastException if the object is not of type K. + */ + TrieEntry getEntry(Object k) { + K key = asKey(k); + if(key == null) + return null; + + int keyLength = length(key); + TrieEntry entry = getNearestEntryForKey(key, keyLength); + return !entry.isEmpty() && key.equals(entry.key) ? entry : null; + } + + /** Gets the key as a 'K'. */ + @SuppressWarnings("unchecked") + protected final K asKey(Object key) { + try { + return (K)key; + } catch(ClassCastException cce) { + // Because the type is erased, the cast & return are + // actually doing nothing, making this CCE impossible. + // However, it's still here on the off-chance it may + // work. + return null; + } + } + + /** + * Returns the nearest entry for a given key. This is useful + * for finding knowing if a given key exists (and finding the value + * for it), or for inserting the key. + * + * The actual get implementation. This is very similar to + * selectR but with the exception that it might return the + * root Entry even if it's empty. + */ + private TrieEntry getNearestEntryForKey(K key, int keyLength) { + TrieEntry current = root.left; + TrieEntry path = root; + while(true) { + if(current.bitIndex <= path.bitIndex) + return current; + + path = current; + if(!isBitSet(key, keyLength, current.bitIndex)) + current = current.left; + else + current = current.right; + } + } + + /** + * Returns the Value whose Key has the longest prefix + * in common with our lookup key. + */ + @SuppressWarnings("unchecked") + public V select(K key) { + int keyLength = length(key); + TrieEntry[] result = new TrieEntry[1]; + if (!selectR(root.left, -1, key, keyLength, result)) { + TrieEntry e = result[0]; + return e.getValue(); + } + return null; + } + + /** + * This is equivalent to the other selectR() method but without + * its overhead because we're selecting only one best matching + * Entry from the Trie. + */ + private boolean selectR(TrieEntry h, int bitIndex, + final K key, final int keyLength, final TrieEntry[] result) { + + if (h.bitIndex <= bitIndex) { + // If we hit the root Node and it is empty + // we have to look for an alternative best + // matching node. + if (!h.isEmpty()) { + result[0] = h; + return false; + } + return true; + } + + if (!isBitSet(key, keyLength, h.bitIndex)) { + if (selectR(h.left, h.bitIndex, key, keyLength, result)) { + return selectR(h.right, h.bitIndex, key, keyLength, result); + } + } else { + if (selectR(h.right, h.bitIndex, key, keyLength, result)) { + return selectR(h.left, h.bitIndex, key, keyLength, result); + } + } + return false; + } + + @SuppressWarnings("unchecked") + public Map.Entry select(K key, Cursor cursor) { + int keyLength = length(key); + TrieEntry[] result = new TrieEntry[]{ null }; + selectR(root.left, -1, key, keyLength, cursor, result); + return result[0]; + } + + private boolean selectR(TrieEntry h, int bitIndex, + final K key, + final int keyLength, + final Cursor cursor, + final TrieEntry[] result) { + + if (h.bitIndex <= bitIndex) { + if(!h.isEmpty()) { + Cursor.SelectStatus ret = cursor.select(h); + switch(ret) { + case REMOVE: + throw new UnsupportedOperationException("cannot remove during select"); + case EXIT: + result[0] = h; + return false; // exit + case REMOVE_AND_EXIT: + TrieEntry entry = new TrieEntry(h.getKey(), h.getValue(), -1); + result[0] = entry; + removeEntry(h); + return false; + case CONTINUE: + // fall through. + } + } + return true; // continue + } + + if (!isBitSet(key, keyLength, h.bitIndex)) { + if (selectR(h.left, h.bitIndex, key, keyLength, cursor, result)) { + return selectR(h.right, h.bitIndex, key, keyLength, cursor, result); + } + } else { + if (selectR(h.right, h.bitIndex, key, keyLength, cursor, result)) { + return selectR(h.left, h.bitIndex, key, keyLength, cursor, result); + } + } + + return false; + } + + /** + * Returns a view of this Trie of all elements that are + * prefixed by the given key. + * + * In a fixed-keysize Trie, this is essentially a 'get' operation. + * + * For example, if the trie contains 'Lime', 'LimeWire', + * 'LimeRadio', 'Lax', 'Later', 'Lake', and 'Lovely', then + * a lookup of 'Lime' would return 'Lime', 'LimeRadio', and 'LimeWire'. + * + * The view that this returns is optimized to have a very efficient + * Iterator. The firstKey, lastKey & size methods must iterate + * over all possible values in order to determine the results. This + * information is cached until the Patricia tree changes. All other + * methods (except Iterator) must compare the given key to the prefix + * to ensure that it is within the range of the view. The Iterator's + * remove method must also relocate the subtree that contains the + * prefixes if the entry holding the subtree is removed or changes. + * Changing the subtree takes O(K) time. + * + * @param key + */ + public SortedMap getPrefixedBy(K key) { + return getPrefixedByBits(key, 0, keyAnalyzer.length(key)); + } + + /** + * Returns a view of this Trie of all elements that are + * prefixed by the length of the key. + * + * Fixed-keysize Tries will not support this operation + * (because all keys will be the same length). + * + * For example, if the trie contains 'Lime', 'LimeWire', + * 'LimeRadio', 'Lax', 'Later', 'Lake', and 'Lovely', then + * a lookup of 'LimePlastics' with a length of 4 would + * return 'Lime', 'LimeRadio', and 'LimeWire'. + * + * The view that this returns is optimized to have a very efficient + * Iterator. The firstKey, lastKey & size methods must iterate + * over all possible values in order to determine the results. This + * information is cached until the Patricia tree changes. All other + * methods (except Iterator) must compare the given key to the prefix + * to ensure that it is within the range of the view. The Iterator's + * remove method must also relocate the subtree that contains the + * prefixes if the entry holding the subtree is removed or changes. + * Changing the subtree takes O(K) time. + * + * @param key + * @param length + */ + public SortedMap getPrefixedBy(K key, int length) { + return getPrefixedByBits(key, 0, length * keyAnalyzer.bitsPerElement()); + } + + /** + * Returns a view of this Trie of all elements that are prefixed + * by the key, starting at the given offset and for the given length. + * + * Fixed-keysize Tries will not support this operation + * (because all keys are the same length). + * + * For example, if the trie contains 'Lime', 'LimeWire', + * 'LimeRadio', 'Lax', 'Later', 'Lake', and 'Lovely', then + * a lookup of 'The Lime Plastics' with an offset of 4 and a + * length of 4 would return 'Lime', 'LimeRadio', and 'LimeWire'. + * + * The view that this returns is optimized to have a very efficient + * Iterator. The firstKey, lastKey & size methods must iterate + * over all possible values in order to determine the results. This + * information is cached until the Patricia tree changes. All other + * methods (except Iterator) must compare the given key to the prefix + * to ensure that it is within the range of the view. The Iterator's + * remove method must also relocate the subtree that contains the + * prefixes if the entry holding the subtree is removed or changes. + * Changing the subtree takes O(K) time. + * + * @param key + * @param offset + * @param length + */ + public SortedMap getPrefixedBy(K key, int offset, int length) { + return getPrefixedByBits(key, offset * keyAnalyzer.bitsPerElement(), length * keyAnalyzer.bitsPerElement()); + } + + /** + * Returns a view of this Trie of all elements that are prefixed + * by the number of bits in the given Key. + * + * Fixed-keysize Tries can support this operation as a way to do + * lookups of partial keys. That is, if the Trie is storing IP + * addresses, you can lookup all addresses that begin with + * '192.168' by providing the key '192.168.X.X' and a length of 16 + * would return all addresses that begin with '192.168'. + * + * The view that this returns is optimized to have a very efficient + * Iterator. The firstKey, lastKey & size methods must iterate + * over all possible values in order to determine the results. This + * information is cached until the Patricia tree changes. All other + * methods (except Iterator) must compare the given key to the prefix + * to ensure that it is within the range of the view. The Iterator's + * remove method must also relocate the subtree that contains the + * prefixes if the entry holding the subtree is removed or changes. + * Changing the subtree takes O(K) time. + * + * @param key + * @param bitLength + */ + public SortedMap getPrefixedByBits(K key, int bitLength) { + return getPrefixedByBits(key, 0, bitLength); + } + + /** + * Returns a view of this map, with entries containing only those that + * are prefixed by a value whose bits matches the bits between 'offset' + * and 'length' in the given key. + * + * The view that this returns is optimized to have a very efficient + * Iterator. The firstKey, lastKey & size methods must iterate + * over all possible values in order to determine the results. This + * information is cached until the Patricia tree changes. All other + * methods (except Iterator) must compare the given key to the prefix + * to ensure that it is within the range of the view. The Iterator's + * remove method must also relocate the subtree that contains the + * prefixes if the entry holding the subtree is removed or changes. + * Changing the subtree takes O(K) time. + * + * @param key + * @param offset + * @param length + */ + private SortedMap getPrefixedByBits(K key, int offset, int length) { + int offsetLength = offset + length; + if (offsetLength > length(key)) { + throw new IllegalArgumentException(offset + " + " + length + " > " + length(key)); + } + + if(offsetLength == 0) + return this; + + return new PrefixSubMap(key, offset, length); + } + + /** + * Returns true if this trie contains the specified Key + * + * This may throw ClassCastException if the object is not + * of type K. + */ + public boolean containsKey(Object k) { + K key = asKey(k); + if(key == null) + return false; + + int keyLength = length(key); + TrieEntry entry = getNearestEntryForKey(key, keyLength); + return !entry.isEmpty() && key.equals(entry.key); + } + + /** Returns true if this Trie contains the specified value. */ + public boolean containsValue(Object o) { + for(V v : values()) + if(valEquals(v, o)) + return true; + return false; + } + + + /** + * Removes a Key from the Trie if one exists + * + * This may throw ClassCastException if the object is not of type K. + * + * @param k the Key to delete + * @return Returns the deleted Value + */ + public V remove(Object k) { + K key = asKey(k); + if(key == null) + return null; + + int keyLength = length(key); + TrieEntry current = root.left; + TrieEntry path = root; + while(true) { + if(current.bitIndex <= path.bitIndex) { + if(!current.isEmpty() && key.equals(current.key)) + return removeEntry(current); + else + return null; + } + + path = current; + if(!isBitSet(key, keyLength, current.bitIndex)) + current = current.left; + else + current = current.right; + } + } + + /** + * Removes a single entry from the Trie. + * + * If we found a Key (Entry h) then figure out if it's + * an internal (hard to remove) or external Entry (easy + * to remove) + */ + private V removeEntry(TrieEntry h) { + if (h != root) { + if (h.isInternalNode()) { + removeInternalEntry(h); + } else { + removeExternalEntry(h); + } + } + + decrementSize(); + return h.setKeyValue(null, null); + } + + /** + * Removes an external entry from the Trie. + * + * If it's an external Entry then just remove it. + * This is very easy and straight forward. + */ + private void removeExternalEntry(TrieEntry h) { + if (h == root) { + throw new IllegalArgumentException("Cannot delete root Entry!"); + } else if (!h.isExternalNode()) { + throw new IllegalArgumentException(h + " is not an external Entry!"); + } + + TrieEntry parent = h.parent; + TrieEntry child = (h.left == h) ? h.right : h.left; + + if (parent.left == h) { + parent.left = child; + } else { + parent.right = child; + } + + // either the parent is changing, or the predecessor is changing. + if (child.bitIndex > parent.bitIndex) { + child.parent = parent; + } else { + child.predecessor = parent; + } + + } + + /** + * Removes an internal entry from the Trie. + * + * If it's an internal Entry then "good luck" with understanding + * this code. The Idea is essentially that Entry p takes Entry h's + * place in the trie which requires some re-wiring. + */ + private void removeInternalEntry(TrieEntry h) { + if (h == root) { + throw new IllegalArgumentException("Cannot delete root Entry!"); + } else if (!h.isInternalNode()) { + throw new IllegalArgumentException(h + " is not an internal Entry!"); + } + + TrieEntry p = h.predecessor; + + // Set P's bitIndex + p.bitIndex = h.bitIndex; + + // Fix P's parent, predecessor and child Nodes + { + TrieEntry parent = p.parent; + TrieEntry child = (p.left == h) ? p.right : p.left; + + // if it was looping to itself previously, + // it will now be pointed from it's parent + // (if we aren't removing it's parent -- + // in that case, it remains looping to itself). + // otherwise, it will continue to have the same + // predecessor. + if(p.predecessor == p && p.parent != h) + p.predecessor = p.parent; + + if (parent.left == p) { + parent.left = child; + } else { + parent.right = child; + } + + if (child.bitIndex > parent.bitIndex) { + child.parent = parent; + } + }; + + // Fix H's parent and child Nodes + { + // If H is a parent of its left and right child + // then change them to P + if (h.left.parent == h) { + h.left.parent = p; + } + + if (h.right.parent == h) { + h.right.parent = p; + } + + // Change H's parent + if (h.parent.left == h) { + h.parent.left = p; + } else { + h.parent.right = p; + } + }; + + // Copy the remaining fields from H to P + //p.bitIndex = h.bitIndex; + p.parent = h.parent; + p.left = h.left; + p.right = h.right; + + // Make sure that if h was pointing to any uplinks, + // p now points to them. + if(isValidUplink(p.left, p)) + p.left.predecessor = p; + if(isValidUplink(p.right, p)) + p.right.predecessor = p; + + } + + /** + * Returns the node lexicographically before the given node (or null if none). + * + * This follows four simple branches: + * - If the uplink that returned us was a right uplink: + * - If predecessor's left is a valid uplink from predecessor, return it. + * - Else, follow the right path from the predecessor's left. + * - If the uplink that returned us was a left uplink: + * - Loop back through parents until we encounter a node where + * node != node.parent.left. + * - If node.parent.left is uplink from node.parent: + * - If node.parent.left is not root, return it. + * - If it is root & root isEmpty, return null. + * - If it is root & root !isEmpty, return root. + * - If node.parent.left is not uplink from node.parent: + * - Follow right path for first right child from node.parent.left + * + * @param start + */ + private TrieEntry previousEntry(TrieEntry start) { + if(start.predecessor == null) + throw new IllegalArgumentException("must have come from somewhere!"); + + if(start.predecessor.right == start) { + if(isValidUplink(start.predecessor.left, start.predecessor)) { + return start.predecessor.left; + } else { + return followRight(start.predecessor.left); + } + } else { + TrieEntry node = start.predecessor; + while(node.parent != null && node == node.parent.left) + node = node.parent; + if(node.parent == null) // can be null if we're looking up root. + return null; + if(isValidUplink(node.parent.left, node.parent)) { + if(node.parent.left == root) { + if(root.isEmpty()) + return null; + else + return root; + } else { + return node.parent.left; + } + } else { + return followRight(node.parent.left); + } + } + } + + /** + * Returns the entry lexicographically after the given entry. + * If the given entry is null, returns the first node. + */ + private TrieEntry nextEntry(TrieEntry node) { + if(node == null) { + return firstEntry(); + } else { + return nextEntryImpl(node.predecessor, node, null); + } + } + + /** + * Returns the entry lexicographically after the given entry. + * If the given entry is null, returns the first node. + * + * This will traverse only within the subtree. If the given node + * is not within the subtree, this will have undefined results. + */ + private TrieEntry nextEntryInSubtree(TrieEntry node, TrieEntry parentOfSubtree) { + if(node == null) { + return firstEntry(); + } else { + return nextEntryImpl(node.predecessor, node, parentOfSubtree); + } + } + + /** + * Scans for the next node, starting at the specified point, and using 'previous' + * as a hint that the last node we returned was 'previous' (so we know not to return + * it again). If 'tree' is non-null, this will limit the search to the given tree. + * + * The basic premise is that each iteration can follow the following steps: + * + * 1) Scan all the way to the left. + * a) If we already started from this node last time, proceed to Step 2. + * b) If a valid uplink is found, use it. + * c) If the result is an empty node (root not set), break the scan. + * d) If we already returned the left node, break the scan. + * + * 2) Check the right. + * a) If we already returned the right node, proceed to Step 3. + * b) If it is a valid uplink, use it. + * c) Do Step 1 from the right node. + * + * 3) Back up through the parents until we encounter find a parent + * that we're not the right child of. + * + * 4) If there's no right child of that parent, the iteration is finished. + * Otherwise continue to Step 5. + * + * 5) Check to see if the right child is a valid uplink. + * a) If we already returned that child, proceed to Step 6. + * Otherwise, use it. + * + * 6) If the right child of the parent is the parent itself, we've + * already found & returned the end of the Trie, so exit. + * + * 7) Do Step 1 on the parent's right child. + */ + private TrieEntry nextEntryImpl(TrieEntry start, TrieEntry previous, TrieEntry tree) { + TrieEntry current = start; + + // Only look at the left if this was a recursive or + // the first check, otherwise we know we've already looked + // at the left. + if(previous == null || start != previous.predecessor) { + while(!current.left.isEmpty()) { + // stop traversing if we've already + // returned the left of this node. + if(previous == current.left) { + break; + } + + if(isValidUplink(current.left, current)) { + return current.left; + } + + current = current.left; + } + } + + // If there's no data at all, exit. + if(current.isEmpty()) { + return null; + } + + // If we've already returned the left, + // and the immediate right is null, + // there's only one entry in the Trie + // which is stored at the root. + // + // / ("") <-- root + // \_/ \ + // null <-- 'current' + // + if(current.right == null) + return null; + + // If nothing valid on the left, try the right. + if(previous != current.right) { + // See if it immediately is valid. + if(isValidUplink(current.right, current)) { + return current.right; + } + + // Must search on the right's side if it wasn't initially valid. + return nextEntryImpl(current.right, previous, tree); + } + + // Neither left nor right are valid, find the first parent + // whose child did not come from the right & traverse it. + while(current == current.parent.right) { + // If we're going to traverse to above the subtree, stop. + if(current == tree) + return null; + + current = current.parent; + } + + // If we're on the top of the subtree, we can't go any higher. + if(current == tree) + return null; + + + // If there's no right, the parent must be root, so we're done. + if(current.parent.right == null) { + return null; + } + + + // If the parent's right points to itself, we've found one. + if(previous != current.parent.right && isValidUplink(current.parent.right, current.parent)) { + return current.parent.right; + } + + // If the parent's right is itself, there can't be any more nodes. + if(current.parent.right == current.parent) { + return null; + } + + // We need to traverse down the parent's right's path. + return nextEntryImpl(current.parent.right, previous, tree); + } + + /** Returns each entry as a string. */ + public String toString() { + StringBuilder buffer = new StringBuilder(); + buffer.append("Trie[").append(size()).append("]={\n"); + for(Iterator> i = newEntryIterator(); i.hasNext(); ) { + buffer.append(" ").append(i.next().toString()).append("\n"); + } + buffer.append("}\n"); + return buffer.toString(); + } + + public Map.Entry traverse(Cursor cursor) { + TrieEntry entry = nextEntry(null); + while(entry != null) { + TrieEntry current = entry; + Cursor.SelectStatus ret = cursor.select(current); + entry = nextEntry(current); + switch(ret) { + case EXIT: + return current; + case REMOVE: + removeEntry(current); + break; // out of switch, stay in while loop + case REMOVE_AND_EXIT: + Map.Entry value = new TrieEntry(current.getKey(), current.getValue(), -1); + removeEntry(current); + return value; + case CONTINUE: // do nothing. + } + } + + return null; + } + + /** Returns true if 'next' is a valid uplink coming from 'from'. */ + private boolean isValidUplink(TrieEntry next, TrieEntry from) { + return next != null && next.bitIndex <= from.bitIndex && !next.isEmpty(); + } + + /** Returns true if bitIndex is a valid index */ + private static boolean isValidBitIndex(int bitIndex) { + return 0 <= bitIndex && bitIndex <= Integer.MAX_VALUE; + } + + /** Returns true if bitIndex is a NULL_BIT_KEY */ + private static boolean isNullBitKey(int bitIndex) { + return bitIndex == KeyAnalyzer.NULL_BIT_KEY; + } + + /** Returns true if bitIndex is a EQUAL_BIT_KEY */ + private static boolean isEqualBitKey(int bitIndex) { + return bitIndex == KeyAnalyzer.EQUAL_BIT_KEY; + } + + /** Returns the length of the key, or 0 if the key is null. */ + private int length(K key) { + if (key == null) { + return 0; + } + + return keyAnalyzer.length(key); + } + + /** + * Returns whether or not the given bit on the + * key is set, or false if the key is null + */ + private boolean isBitSet(K key, int keyLength, int bitIndex) { + if (key == null) { // root's might be null! + return false; + } + return keyAnalyzer.isBitSet(key, keyLength, bitIndex); + } + + /** + * Utility method for calling + * keyAnalyzer.bitIndex(key, 0, length(key), foundKey, 0, length(foundKey)) + */ + private int bitIndex(K key, K foundKey) { + return keyAnalyzer.bitIndex(key, 0, length(key), foundKey, 0, length(foundKey)); + } + + /** The actual Trie nodes. */ + private static class TrieEntry implements Map.Entry, Serializable { + + private static final long serialVersionUID = 4596023148184140013L; + + private K key; + private V value; + + /** The index this entry is comparing. */ + private int bitIndex; + + /** The parent of this entry. */ + private TrieEntry parent; + /** The left child of this entry. */ + private TrieEntry left; + /** The right child of this entry. */ + private TrieEntry right; + /** The entry who uplinks to this entry. */ + private TrieEntry predecessor; + + private TrieEntry(K key, V value, int bitIndex) { + this.key = key; + this.value = value; + + this.bitIndex = bitIndex; + + this.parent = null; + this.left = this; + this.right = null; + this.predecessor = this; + } + + public boolean equals(Object o) { + if(o == this) { + return true; + } else if(o instanceof Map.Entry) { + Map.Entry e = (Map.Entry)o; + Object k1 = getKey(); + Object k2 = e.getKey(); + if (k1 == k2 || (k1 != null && k1.equals(k2))) { + Object v1 = getValue(); + Object v2 = e.getValue(); + if (v1 == v2 || (v1 != null && v1.equals(v2))) + return true; + } + return false; + } else { + return false; + } + } + + /** + * Whether or not the entry is storing a key. + * Only the root can potentially be empty, all other + * nodes must have a key. + */ + public boolean isEmpty() { + return key == null; + } + + public K getKey() { + return key; + } + + public V getValue() { + return value; + } + + public V setValue(V value) { + V o = this.value; + this.value = value; + return o; + } + + /** + * Replaces the old key and value with the new ones. + * Returns the old vlaue. + */ + private V setKeyValue(K key, V value) { + this.key = key; + return setValue(value); + } + + /** Neither the left nor right child is a loopback */ + private boolean isInternalNode() { + return left != this && right != this; + } + + /** Either the left or right child is a loopback */ + private boolean isExternalNode() { + return !isInternalNode(); + } + + public String toString() { + StringBuilder buffer = new StringBuilder(); + + if (bitIndex == -1) { + buffer.append("RootEntry("); + } else { + buffer.append("Entry("); + } + + buffer.append("key=").append(getKey()).append(" [").append(bitIndex).append("], "); + buffer.append("value=").append(getValue()).append(", "); + //buffer.append("bitIndex=").append(bitIndex).append(", "); + + if (parent != null) { + if (parent.bitIndex == -1) { + buffer.append("parent=").append("ROOT"); + } else { + buffer.append("parent=").append(parent.getKey()).append(" [").append(parent.bitIndex).append("]"); + } + } else { + buffer.append("parent=").append("null"); + } + buffer.append(", "); + + if (left != null) { + if (left.bitIndex == -1) { + buffer.append("left=").append("ROOT"); + } else { + buffer.append("left=").append(left.getKey()).append(" [").append(left.bitIndex).append("]"); + } + } else { + buffer.append("left=").append("null"); + } + buffer.append(", "); + + if (right != null) { + if (right.bitIndex == -1) { + buffer.append("right=").append("ROOT"); + } else { + buffer.append("right=").append(right.getKey()).append(" [").append(right.bitIndex).append("]"); + } + } else { + buffer.append("right=").append("null"); + } + buffer.append(", "); + + if(predecessor != null) { + if(predecessor.bitIndex == -1) { + buffer.append("predecessor=").append("ROOT"); + } else { + buffer.append("predecessor=").append(predecessor.getKey()).append(" [").append(predecessor.bitIndex).append("]"); + } + } + + buffer.append(")"); + return buffer.toString(); + } + } + + /** + * Defines the interface to analyze {@link Trie} keys on a bit + * level. KeyAnalyzer's + * methods return the length of the key in bits, whether or not a bit is + * set, and bits per element in the key. + *

+ * Additionally, a method determines if a key is a prefix of another key and + * returns the bit index where one key is different from another key (if + * the key and found key are equal than the return value is EQUAL_BIT_KEY). + *

+ * KeyAnalyzer defines:
+ * + * + * + *
NULL_BIT_KEYWhen key's bits are all zero
EQUAL_BIT_KEY When keys are the same
+ */ + public static interface KeyAnalyzer extends Comparator, Serializable { + + /** Returned by bitIndex if key's bits are all 0 */ + public static final int NULL_BIT_KEY = -1; + + /** + * Returned by bitIndex if key and found key are + * equal. This is a very very specific case and + * shouldn't happen on a regular basis + */ + public static final int EQUAL_BIT_KEY = -2; + + /** + * Returns the length of the Key in bits. + */ + public int length(K key); + + /** Returns whether or not a bit is set */ + public boolean isBitSet(K key, int keyLength, int bitIndex); + + /** + * Returns the n-th different bit between key and found. + * This starts the comparison in key at 'keyStart' and goes + * for 'keyLength' bits, and compares to the found key + * starting at 'foundStart' and going for 'foundLength' bits. + */ + public int bitIndex(K key, int keyStart, int keyLength, + K found, int foundStart, int foundLength); + + /** + * Returns the number of bits per element in the key. + * This is only useful for variable-length keys, such as Strings. + */ + public int bitsPerElement(); + + /** + * Determines whether or not the given prefix (from offset to length) + * is a prefix of the given key. + */ + public boolean isPrefix(K prefix, int offset, int length, K key); + } + + /** An iterator that stores a single TrieEntry. */ + private class SingletonIterator implements Iterator> { + private final TrieEntry entry; + private int hit = 0; + + public SingletonIterator(TrieEntry entry) { + this.entry = entry; + } + + public boolean hasNext() { + return hit == 0; + } + + public Map.Entry next() { + if(hit != 0) + throw new NoSuchElementException(); + hit++; + return entry; + } + + public void remove() { + if(hit != 1) + throw new IllegalStateException(); + hit++; + PatriciaTrie.this.removeEntry(entry); + } + + } + + /** An iterator for the entries. */ + private abstract class NodeIterator implements Iterator { + protected int expectedModCount = modCount; // For fast-fail + protected TrieEntry next; // the next node to return + protected TrieEntry current; // the current entry we're on + + // Starts iteration from the beginning. + protected NodeIterator() { + next = PatriciaTrie.this.nextEntry(null); + } + + // Starts iteration at the given entry. + protected NodeIterator(TrieEntry firstEntry) { + next = firstEntry; + } + + public boolean hasNext() { + return next != null; + } + + TrieEntry nextEntry() { + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + TrieEntry e = next; + if (e == null) + throw new NoSuchElementException(); + + next = findNext(e); + current = e; + return e; + } + + protected TrieEntry findNext(TrieEntry prior) { + return PatriciaTrie.this.nextEntry(prior); + } + + public void remove() { + if (current == null) + throw new IllegalStateException(); + if (modCount != expectedModCount) + throw new ConcurrentModificationException(); + + TrieEntry node = current; + current = null; + PatriciaTrie.this.removeEntry(node); + + expectedModCount = modCount; + } + } + + private class ValueIterator extends NodeIterator { + public V next() { + return nextEntry().value; + } + } + + private class KeyIterator extends NodeIterator { + public K next() { + return nextEntry().getKey(); + } + } + + private class EntryIterator extends NodeIterator> { + public Map.Entry next() { + return nextEntry(); + } + } + + /** An iterator for iterating over a prefix search. */ + private class PrefixEntryIterator extends NodeIterator> { + // values to reset the subtree if we remove it. + protected final K prefix; + protected final int offset; + protected final int length; + protected boolean lastOne; + + protected TrieEntry subtree; // the subtree to search within + + // Starts iteration at the given entry & search only within the given subtree. + PrefixEntryIterator(TrieEntry startScan, K prefix, int offset, int length) { + subtree = startScan; + next = PatriciaTrie.this.followLeft(startScan); + this.prefix = prefix; + this.offset = offset; + this.length = length; + } + + public Map.Entry next() { + Map.Entry entry = nextEntry(); + if(lastOne) + next = null; + return entry; + } + + @Override + protected TrieEntry findNext(TrieEntry prior) { + return PatriciaTrie.this.nextEntryInSubtree(prior, subtree); + } + + @Override + public void remove() { + // If the current entry we're removing is the subtree + // then we need to find a new subtree parent. + boolean needsFixing = false; + int bitIdx = subtree.bitIndex; + if(current == subtree) + needsFixing = true; + + super.remove(); + + // If the subtree changed its bitIndex or we + // removed the old subtree, get a new one. + if(bitIdx != subtree.bitIndex || needsFixing) + subtree = subtree(prefix, offset, length); + + // If the subtree's bitIndex is less than the + // length of our prefix, it's the last item + // in the prefix tree. + if(length >= subtree.bitIndex) + lastOne = true; + } + + } + + /** An iterator for submaps. */ + private class SubMapEntryIterator extends NodeIterator> { + private final K firstExcludedKey; + + SubMapEntryIterator(TrieEntry first, TrieEntry firstExcluded) { + super(first); + firstExcludedKey = + (firstExcluded == null ? null : firstExcluded.key); + } + + public boolean hasNext() { + return next != null && next.key != firstExcludedKey; + } + + public Map.Entry next() { + if (next == null || next.key == firstExcludedKey) + throw new NoSuchElementException(); + return nextEntry(); + } + } + + Iterator newKeyIterator() { + return new KeyIterator(); + } + + Iterator newValueIterator() { + return new ValueIterator(); + } + + Iterator> newEntryIterator() { + return new EntryIterator(); + } + + /** + * Each of these fields are initialized to contain an instance of the + * appropriate view the first time this view is requested. The views are + * stateless, so there's no reason to create more than one of each. + */ + private transient volatile Set keySet = null; + private transient volatile Collection values = null; + private transient volatile Set> entrySet = null; + + private class EntrySet extends AbstractSet> { + public Iterator> iterator() { + return newEntryIterator(); + } + + public boolean contains(Object o) { + if (!(o instanceof Map.Entry)) + return false; + + TrieEntry candidate = getEntry(((Map.Entry)o).getKey()); + return candidate != null && candidate.equals(o); + } + + public boolean remove(Object o) { + int size = size(); + PatriciaTrie.this.remove(o); + return size != size(); + } + + public int size() { + return size; + } + + public void clear() { + PatriciaTrie.this.clear(); + } + } + + /** + * Returns a set view of the keys contained in this map. The set is + * backed by the map, so changes to the map are reflected in the set, and + * vice-versa. The set supports element removal, which removes the + * corresponding mapping from this map, via the Iterator.remove, + * Set.remove, removeAll, retainAll, and + * clear operations. It does not support the add or + * addAll operations. + * + * @return a set view of the keys contained in this map. + */ + public Set keySet() { + Set ks = keySet; + return (ks != null ? ks : (keySet = new KeySet())); + } + + private class KeySet extends AbstractSet { + public Iterator iterator() { + return newKeyIterator(); + } + public int size() { + return size; + } + public boolean contains(Object o) { + return containsKey(o); + } + public boolean remove(Object o) { + int size = size(); + PatriciaTrie.this.remove(o); + return size != size(); + } + public void clear() { + PatriciaTrie.this.clear(); + } + } + + /** + * Returns a collection view of the values contained in this map. The + * collection is backed by the map, so changes to the map are reflected in + * the collection, and vice-versa. The collection supports element + * removal, which removes the corresponding mapping from this map, via the + * Iterator.remove, Collection.remove, + * removeAll, retainAll, and clear operations. + * It does not support the add or addAll operations. + * + * @return a collection view of the values contained in this map. + */ + public Collection values() { + Collection vs = values; + return (vs != null ? vs : (values = new Values())); + } + + /** Test two values for equality. Works with null values. */ + private static boolean valEquals(Object o1, Object o2) { + return (o1==null ? o2==null : o1.equals(o2)); + } + + private class Values extends AbstractCollection { + public Iterator iterator() { + return newValueIterator(); + } + public int size() { + return size; + } + public boolean contains(Object o) { + return containsValue(o); + } + public void clear() { + PatriciaTrie.this.clear(); + } + public boolean remove(Object o) { + for(Iterator i = iterator(); i.hasNext(); ) { + V v = i.next(); + if(valEquals(v, o)) { + i.remove(); + return true; + } + } + return false; + } + } + + /** + * Returns the first entry the Trie is storing. + * + * This is implemented by going always to the left until + * we encounter a valid uplink. That uplink is the first key. + */ + private TrieEntry firstEntry() { + // if Trie is empty, no first node. + if(isEmpty()) + return null; + + return followLeft(root); + } + + /** Goes left through the tree until it finds a valid node. */ + private TrieEntry followLeft(TrieEntry node) { + while(true) { + TrieEntry child = node.left; + // if we hit root and it didn't have a node, go right instead. + if(child.isEmpty()) + child = node.right; + + if(child.bitIndex <= node.bitIndex) + return child; + + node = child; + } + } + + /** + * Returns the last entry the Trie is storing. + *

+ * This is implemented by going always to the right until + * we encounter a valid uplink. That uplink is the last key. + */ + private TrieEntry lastEntry() { + return followRight(root.left); + } + + /** Traverses down the right path until it finds an uplink. */ + protected TrieEntry followRight(TrieEntry node) { + // if Trie is empty, no last entry. + if(node.right == null) + return null; + + // Go as far right as possible, until we encounter an uplink. + while(node.right.bitIndex > node.bitIndex) + node = node.right; + + return node.right; + } + + public K firstKey() { + return firstEntry().getKey(); + } + + public SortedMap headMap(K toKey) { + return new SubMap(null, toKey); + } + + public K lastKey() { + TrieEntry entry = lastEntry(); + if(entry != null) + return entry.getKey(); + else + return null; + } + + + public SortedMap subMap(K fromKey, K toKey) { + return new SubMap(fromKey, toKey); + } + + public SortedMap tailMap(K fromKey) { + return new SubMap(fromKey, null); + } + + /** + * Returns an entry strictly higher than the given key, + * or null if no such entry exists. + */ + protected TrieEntry higherEntry(K key) { + // TODO: Cleanup so that we don't actually have to add/remove from the + // tree. (We do it here because there are other well-defined + // functions to perform the search.) + int keyLength = length(key); + + if (keyLength == 0) { + if(!root.isEmpty()) { + // If data in root, and more after -- return it. + if(size() > 1) { + return nextEntry(root); + } else { // If no more after, no higher entry. + return null; + } + } else { + // Root is empty & we want something after empty, return first. + return firstEntry(); + } + } + + TrieEntry found = getNearestEntryForKey(key, keyLength); + if (key.equals(found.key)) + return nextEntry(found); + + int bitIndex = bitIndex(key, found.key); + if (isValidBitIndex(bitIndex)) { + TrieEntry added = new TrieEntry(key, null, bitIndex); + addEntry(added, keyLength); + incrementSize(); // must increment because remove will decrement + TrieEntry ceil = nextEntry(added); + removeEntry(added); + modCount -= 2; // we didn't really modify it. + return ceil; + } else if (isNullBitKey(bitIndex)) { + if (!root.isEmpty()) + return firstEntry(); + else if(size() > 1) + return nextEntry(firstEntry()); + else + return null; + } else if (isEqualBitKey(bitIndex)) { + return nextEntry(found); + } + + // we should have exited above. + throw new IllegalStateException("invalid lookup: " + key); + } + + /** + * Returns a key-value mapping associated with the least key greater + * than or equal to the given key, or null if there is no such key. + */ + protected TrieEntry ceilingEntry(K key) { + // Basically: + // Follow the steps of adding an entry, but instead... + // + // - If we ever encounter a situation where we found an equal + // key, we return it immediately. + // + // - If we hit an empty root, return the first iterable item. + // + // - If we have to add a new item, we temporarily add it, + // find the successor to it, then remove the added item. + // + // These steps ensure that the returned value is either the + // entry for the key itself, or the first entry directly after + // the key. + + // TODO: Cleanup so that we don't actually have to add/remove from the + // tree. (We do it here because there are other well-defined + // functions to perform the search.) + int keyLength = length(key); + + if (keyLength == 0) { + if(!root.isEmpty()) + return root; + else + return firstEntry(); + } + + TrieEntry found = getNearestEntryForKey(key, keyLength); + if (key.equals(found.key)) + return found; + + int bitIndex = bitIndex(key, found.key); + if (isValidBitIndex(bitIndex)) { + TrieEntry added = new TrieEntry(key, null, bitIndex); + addEntry(added, keyLength); + incrementSize(); // must increment because remove will decrement + TrieEntry ceil = nextEntry(added); + removeEntry(added); + modCount -= 2; // we didn't really modify it. + return ceil; + } else if (isNullBitKey(bitIndex)) { + if (!root.isEmpty()) + return root; + else + return firstEntry(); + } else if (isEqualBitKey(bitIndex)) { + return found; + } + + // we should have exited above. + throw new IllegalStateException("invalid lookup: " + key); + } + + /** + * Returns a key-value mapping associated with the greatest key + * strictly less than the given key, or null if there is no such key. + */ + protected TrieEntry lowerEntry(K key) { + // Basically: + // Follow the steps of adding an entry, but instead... + // + // - If we ever encounter a situation where we found an equal + // key, we return it's previousEntry immediately. + // + // - If we hit root (empty or not), return null. + // + // - If we have to add a new item, we temporarily add it, + // find the previousEntry to it, then remove the added item. + // + // These steps ensure that the returned value is always just before + // the key or null (if there was nothing before it). + + // TODO: Cleanup so that we don't actually have to add/remove from the + // tree. (We do it here because there are other well-defined + // functions to perform the search.) + int keyLength = length(key); + + if (keyLength == 0) { + return null; // there can never be anything before root. + } + + TrieEntry found = getNearestEntryForKey(key, keyLength); + if (key.equals(found.key)) + return previousEntry(found); + + int bitIndex = bitIndex(key, found.key); + if (isValidBitIndex(bitIndex)) { + TrieEntry added = new TrieEntry(key, null, bitIndex); + addEntry(added, keyLength); + incrementSize(); // must increment because remove will decrement + TrieEntry prior = previousEntry(added); + removeEntry(added); + modCount -= 2; // we didn't really modify it. + return prior; + } else if (isNullBitKey(bitIndex)) { + return null; + } else if (isEqualBitKey(bitIndex)) { + return previousEntry(found); + } + + // we should have exited above. + throw new IllegalStateException("invalid lookup: " + key); + } + + /** + * Returns a key-value mapping associated with the greatest key + * less than or equal to the given key, or null if there is no such key. + */ + protected TrieEntry floorEntry(K key) { + // TODO: Cleanup so that we don't actually have to add/remove from the + // tree. (We do it here because there are other well-defined + // functions to perform the search.) + int keyLength = length(key); + + if (keyLength == 0) { + if(!root.isEmpty()) + return root; + else + return null; + } + + TrieEntry found = getNearestEntryForKey(key, keyLength); + if (key.equals(found.key)) + return found; + + int bitIndex = bitIndex(key, found.key); + if (isValidBitIndex(bitIndex)) { + TrieEntry added = new TrieEntry(key, null, bitIndex); + addEntry(added, keyLength); + incrementSize(); // must increment because remove will decrement + TrieEntry floor = previousEntry(added); + removeEntry(added); + modCount -= 2; // we didn't really modify it. + return floor; + } else if (isNullBitKey(bitIndex)) { + if (!root.isEmpty()) + return root; + else + return null; + } else if (isEqualBitKey(bitIndex)) { + return found; + } + + // we should have exited above. + throw new IllegalStateException("invalid lookup: " + key); + } + + + /** + * Finds the subtree that contains the prefix. + * + * This is very similar to getR but with the difference that + * we stop the lookup if h.bitIndex > keyLength. + */ + private TrieEntry subtree(K prefix, int offset, int length) { + TrieEntry current = root.left; + TrieEntry path = root; + while(true) { + if(current.bitIndex <= path.bitIndex || length < current.bitIndex) + break; + + path = current; + if(!isBitSet(prefix, length+offset, current.bitIndex+offset)) { + current = current.left; + } else { + current = current.right; + } + } + + // Make sure the entry is valid for a subtree. + TrieEntry entry = current.isEmpty() ? path : current; + + // If entry is root, it can't be empty. + if (entry.isEmpty()) + return null; + + int offsetLength = offset + length; + + // if root && length of root is less than length of lookup, + // there's nothing. + // (this prevents returning the whole subtree if root has an empty + // string and we want to lookup things with "\0") + if(entry == root && length(entry.getKey()) < offsetLength) + return null; + + // Found key's length-th bit differs from our key + // which means it cannot be the prefix... + if(isBitSet(prefix, offsetLength, offsetLength) != + isBitSet(entry.key, length(entry.key), length)) { + return null; + } + + // ... or there are less than 'length' equal bits + int bitIndex = keyAnalyzer.bitIndex(prefix, offset, length, + entry.key, 0, length(entry.getKey())); + if (bitIndex >= 0 && bitIndex < length) + return null; + + return entry; + } + + /** A submap used for prefix views over the Trie. */ + private class PrefixSubMap extends SubMap { + protected final K prefix; + protected final int offset; + protected final int length; + private transient int keyModCount = 0; + protected int size; + + PrefixSubMap(K prefix, int offset, int length) { + this.prefix = prefix; + this.offset = offset; + this.length = length; + fromInclusive = false; + } + + public K firstKey() { + fixup(); + TrieEntry e; + if(fromKey == null) { + e = firstEntry(); + } else { + e = higherEntry(fromKey); + } + + K first = e != null ? e.getKey() : null; + if (e == null || !keyAnalyzer.isPrefix(prefix, offset, length, first)) + throw new NoSuchElementException(); + return first; + } + + public K lastKey() { + fixup(); + TrieEntry e; + if(toKey == null) { + e = lastEntry(); + } else { + e = lowerEntry(toKey); + } + + K last = e != null ? e.getKey() : null; + if (e == null || !keyAnalyzer.isPrefix(prefix, offset, length, last)) + throw new NoSuchElementException(); + return last; + } + + protected boolean inRange(K key) { + return keyAnalyzer.isPrefix(prefix, offset, length, key); + } + + protected boolean inRange2(K key) { + return keyAnalyzer.isPrefix(prefix, offset, length, key); + } + + protected boolean inToRange(K key, boolean forceInclusive) { + return keyAnalyzer.isPrefix(prefix, offset, length, key); + } + + protected boolean inFromRange(K key, boolean forceInclusive) { + return keyAnalyzer.isPrefix(prefix, offset, length, key); + } + + private void fixup() { + // The trie has changed since we last + // found our toKey / fromKey + if(modCount != keyModCount) { + Iterator> iter = entrySet().iterator(); + size = 0; + + Map.Entry entry = null; + if(iter.hasNext()) { + entry = iter.next(); + size = 1; + } + + fromKey = entry == null ? null : entry.getKey(); + if(fromKey != null) { + TrieEntry prior = previousEntry((TrieEntry)entry); + fromKey = prior == null ? null : prior.getKey(); + } + + toKey = fromKey; + + while(iter.hasNext()) { + size++; + entry = iter.next(); + } + + toKey = entry == null ? null : entry.getKey(); + + if(toKey != null) { + entry = nextEntry((TrieEntry)entry); + toKey = entry == null ? null : entry.getKey(); + } + + keyModCount = modCount; + } + } + + protected Set> newSubMapEntrySet() { + return new PrefixEntrySetView(); + } + + private class PrefixEntrySetView extends SubMap.EntrySetView { + private TrieEntry prefixStart; + private int iterModCount = 0; + + public int size() { + fixup(); + return PrefixSubMap.this.size; + } + + @SuppressWarnings("unchecked") + public Iterator> iterator() { + if(modCount != iterModCount) { + prefixStart = subtree(prefix, offset, length); + iterModCount = modCount; + } + + if(prefixStart == null) { + return new EmptyIterator(); + } else if(length >= prefixStart.bitIndex){ + return new SingletonIterator(prefixStart); + } else { + return new PrefixEntryIterator(prefixStart, prefix, offset, length); + } + } + } + } + + private class SubMap extends AbstractMap implements SortedMap, java.io.Serializable { + + // TODO: add serialVersionUID + + /** The key to start from, null if the beginning. */ + protected K fromKey; + + /** The key to end at, null if till the end. */ + protected K toKey; + + /** Whether or not the 'from' is inclusive. */ + protected boolean fromInclusive; + + /** Whether or not the 'to' is inclusive. */ + protected boolean toInclusive; + + /** + * Constructs a blank SubMap -- this should ONLY be used + * by subclasses that wish to lazily construct their + * fromKey or toKey + */ + protected SubMap() {} + + SubMap(K fromKey, K toKey) { + if(fromKey == null && toKey == null) + throw new IllegalArgumentException("must have a from or to!"); + if(fromKey != null && toKey != null && keyAnalyzer.compare(fromKey, toKey) > 0) + throw new IllegalArgumentException("fromKey > toKey"); + this.fromKey = fromKey; + this.toKey = toKey; + fromInclusive = true; + } + + public boolean isEmpty() { + return entrySet().isEmpty(); + } + + @SuppressWarnings("unchecked") + public boolean containsKey(Object key) { + return inRange((K) key) && PatriciaTrie.this.containsKey(key); + } + + @SuppressWarnings("unchecked") + public V remove(Object key) { + if(!inRange((K)key)) + return null; + return PatriciaTrie.this.remove(key); + } + + @SuppressWarnings("unchecked") + public V get(Object key) { + if (!inRange((K) key)) + return null; + return PatriciaTrie.this.get(key); + } + + public V put(K key, V value) { + if (!inRange(key)) + throw new IllegalArgumentException("key out of range"); + return PatriciaTrie.this.put(key, value); + } + + public Comparator comparator() { + return keyAnalyzer; + } + + public K firstKey() { + TrieEntry e; + if(fromKey == null) { + e = firstEntry(); + } else { + if(fromInclusive) + e = ceilingEntry(fromKey); + else + e = higherEntry(fromKey); + } + + K first = e != null ? e.getKey() : null; + if (e == null || toKey != null && !inToRange(first, false)) + throw new NoSuchElementException(); + return first; + } + + public K lastKey() { + TrieEntry e; + if(toKey == null) { + e = lastEntry(); + } else { + if(toInclusive) + e = floorEntry(toKey); + else + e = lowerEntry(toKey); + } + + K last = e != null ? e.getKey() : null; + if (e == null || fromKey != null && !inFromRange(last, false)) + throw new NoSuchElementException(); + return last; + } + + private transient Set> entrySet; + + public Set> entrySet() { + if(entrySet == null) + entrySet = newSubMapEntrySet(); + return entrySet; + } + + protected Set> newSubMapEntrySet() { + return new EntrySetView(); + } + + class EntrySetView extends AbstractSet> { + private transient int size = -1, sizeModCount; + + public int size() { + if (size == -1 || sizeModCount != PatriciaTrie.this.modCount) { + size = 0; sizeModCount = PatriciaTrie.this.modCount; + Iterator i = iterator(); + while (i.hasNext()) { + size++; + i.next(); + } + } + return size; + } + + public boolean isEmpty() { + return !iterator().hasNext(); + } + + @SuppressWarnings("unchecked") + public boolean contains(Object o) { + if (!(o instanceof Map.Entry)) + return false; + Map.Entry entry = (Map.Entry) o; + K key = entry.getKey(); + if (!inRange(key)) + return false; + TrieEntry node = getEntry(key); + return node != null && + valEquals(node.getValue(), entry.getValue()); + } + + @SuppressWarnings("unchecked") + public boolean remove(Object o) { + if (!(o instanceof Map.Entry)) + return false; + Map.Entry entry = (Map.Entry) o; + K key = entry.getKey(); + if (!inRange(key)) + return false; + TrieEntry node = getEntry(key); + if (node!=null && valEquals(node.getValue(),entry.getValue())){ + removeEntry(node); + return true; + } + return false; + } + + public Iterator> iterator() { + return new SubMapEntryIterator( + (fromKey == null ? firstEntry() : ceilingEntry(fromKey)), + (toKey == null ? null : ceilingEntry(toKey))); + } + } + + public SortedMap subMap(K fromKey, K toKey) { + if (!inRange2(fromKey)) + throw new IllegalArgumentException("fromKey out of range"); + if (!inRange2(toKey)) + throw new IllegalArgumentException("toKey out of range"); + return new SubMap(fromKey, toKey); + } + + public SortedMap headMap(K toKey) { + if (!inRange2(toKey)) + throw new IllegalArgumentException("toKey out of range"); + return new SubMap(fromKey, toKey); + } + + public SortedMap tailMap(K fromKey) { + if (!inRange2(fromKey)) + throw new IllegalArgumentException("fromKey out of range"); + return new SubMap(fromKey, toKey); + } + + protected boolean inRange(K key) { + return (fromKey == null || inFromRange(key, false)) && + (toKey == null || inToRange(key, false)); + } + + // This form allows the high endpoint (as well as all legit keys) + protected boolean inRange2(K key) { + return (fromKey == null || inFromRange(key, false)) && + (toKey == null || inToRange(key, true)); + } + + protected boolean inToRange(K key, boolean forceInclusive) { + int ret = keyAnalyzer.compare(key, toKey); + if(toInclusive || forceInclusive) + return ret <= 0; + else + return ret < 0; + } + + protected boolean inFromRange(K key, boolean forceInclusive) { + int ret = keyAnalyzer.compare(key, fromKey); + if (fromInclusive || forceInclusive) + return ret >= 0; + else + return ret > 0; + } + } + + private static class EmptyIterator implements Iterator { + public boolean hasNext() { + return false; + } + + public Object next() { + throw new NoSuchElementException(); + } + + public void remove() { + throw new IllegalStateException(); + } + + } +} + diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/trie/Trie.java b/plugins/spellchecker/src/com/intellij/spellchecker/trie/Trie.java new file mode 100644 index 000000000000..49aadb369124 --- /dev/null +++ b/plugins/spellchecker/src/com/intellij/spellchecker/trie/Trie.java @@ -0,0 +1,172 @@ +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.trie; + + +import java.util.Map; +import java.util.SortedMap; + +/** + * Defines the interface for a prefix tree, an ordered tree data structure. For + * more information, see Tries. + * + * @author Roger Kapsi + * @author Sam Berlin + */ +public interface Trie extends SortedMap { + + /** + * Returns a view of this Trie of all elements that are + * prefixed by the given key. + *

+ * In a fixed-keysize Trie, this is essentially a 'get' operation. + *

+ * For example, if the Trie contains 'Lime', 'LimeWire', + * 'LimeRadio', 'Lax', 'Later', 'Lake', and 'Lovely', then + * a lookup of 'Lime' would return 'Lime', 'LimeRadio', and 'LimeWire'. + */ + public SortedMap getPrefixedBy(K key); + + /** + * Returns a view of this Trie of all elements that are + * prefixed by the length of the key. + *

+ * Fixed-keysize Tries will not support this operation + * (because all keys will be the same length). + *

+ * For example, if the Trie contains 'Lime', 'LimeWire', + * 'LimeRadio', 'Lax', 'Later', 'Lake', and 'Lovely', then + * a lookup of 'LimePlastics' with a length of 4 would + * return 'Lime', 'LimeRadio', and 'LimeWire'. + */ + public SortedMap getPrefixedBy(K key, int length); + + /** + * Returns a view of this Trie of all elements that are prefixed + * by the key, starting at the given offset and for the given length. + *

+ * Fixed-keysize Tries will not support this operation + * (because all keys are the same length). + *

+ * For example, if the Trie contains 'Lime', 'LimeWire', + * 'LimeRadio', 'Lax', 'Later', 'Lake', and 'Lovely', then + * a lookup of 'The Lime Plastics' with an offset of 4 and a + * length of 4 would return 'Lime', 'LimeRadio', and 'LimeWire'. + */ + public SortedMap getPrefixedBy(K key, int offset, int length); + + /** + * Returns a view of this Trie of all elements that are prefixed + * by the number of bits in the given Key. + *

+ * Fixed-keysize Tries can support this operation as a way to do + * lookups of partial keys. That is, if the Trie is storing IP + * addresses, you can lookup all addresses that begin with + * '192.168' by providing the key '192.168.X.X' and a length of 16 + * would return all addresses that begin with '192.168'. + */ + public SortedMap getPrefixedByBits(K key, int bitLength); + + /** + * Returns the value for the entry whose key is closest in a bitwise + * XOR metric to the given key. This is NOT lexicographic closeness. + * For example, given the keys:
+ * D = 1000100
+ * H = 1001000
+ * L = 1001100
+ *

+ * If the Trie contained 'H' and 'L', a lookup of 'D' would return 'L', + * because the XOR distance between D & L is smaller than the XOR distance + * between D & H. + */ + public V select(K key); + + /** + * Iterates through the Trie, starting with the entry whose bitwise + * value is closest in an XOR metric to the given key. After the closest + * entry is found, the Trie will call select on that entry and continue + * calling select for each entry (traversing in order of XOR closeness, + * NOT lexicographically) until the cursor returns + * Cursor.SelectStatus.EXIT.
+ * The cursor can return Cursor.SelectStatus.CONTINUE to + * continue traversing.
+ * Cursor.SelectStatus.REMOVE_AND_EXIT is used to remove the current element + * and stop traversing. + *

+ * Note: The {@link Cursor.SelectStatus#REMOVE} operation is not supported. + * + * @return The entry the cursor returned EXIT on, or null if it continued + * till the end. + */ + public Map.Entry select(K key, Cursor cursor); + + /** + * Traverses the Trie in lexicographical order. Cursor.select + * will be called on each entry.

+ * The traversal will stop when the cursor returns Cursor.SelectStatus.EXIT.
+ * Cursor.SelectStatus.CONTINUE is used to continue traversing.
+ * Cursor.SelectStatus.REMOVE is used to remove the element that was + * selected and continue traversing.
+ * Cursor.SelectStatus.REMOVE_AND_EXIT is used to remove the current element + * and stop traversing. + * + * @return The entry the cursor returned EXIT on, or null if it continued + * till the end. + */ + public Map.Entry traverse(Cursor cursor); + + /** + * An interface used by a {@link Trie}. A {@link Trie} selects items by + * closeness and passes the items to the Cursor. You can then + * decide what to do with the key-value pair and the return value + * from {@link #select(java.util.Map.Entry)} tells the Trie + * what to do next. + *

+ * Cursor returns status/selection status might be: + * + * + * + * + * + * + *
Return ValueStatus
EXITFinish the Trie operation
CONTINUELook at the next element in the traversal
REMOVE_AND_EXITRemove the entry and stop iterating
REMOVERemove the entry and continue iterating
+ + * Note: {@link Trie#select(Object, org.limewire.collection.Trie.Cursor)} does + * not support REMOVE. + * + * @param Key Type + * @param Key Value + */ + public static interface Cursor { + + /** + * Notification that the Trie is currently looking at the given entry. + * Return EXIT to finish the Trie operation, + * CONTINUE to look at the next entry, REMOVE + * to remove the entry and continue iterating, or + * REMOVE_AND_EXIT to remove the entry and stop iterating. + * Not all operations support REMOVE. + * + */ + public SelectStatus select(Map.Entry entry); + + /** The mode during selection. */ + public static enum SelectStatus { + EXIT, CONTINUE, REMOVE, REMOVE_AND_EXIT; + } + } +} + diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/util/SpellCheckerBundle.java b/plugins/spellchecker/src/com/intellij/spellchecker/util/SpellCheckerBundle.java index 307e51c19038..4ecb76209c44 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/util/SpellCheckerBundle.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/util/SpellCheckerBundle.java @@ -1,36 +1,37 @@ -/* - * Copyright 2000-2009 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.intellij.spellchecker.util; - -import com.intellij.CommonBundle; -import org.jetbrains.annotations.NonNls; -import org.jetbrains.annotations.PropertyKey; - -import java.util.ResourceBundle; - - -public final class SpellCheckerBundle { - @NonNls - private static final String BUNDLE_NAME = "com.intellij.spellchecker.util.SpellCheckerBundle"; - private static final ResourceBundle BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME); - - private SpellCheckerBundle() { - } - - public static String message(@PropertyKey(resourceBundle = BUNDLE_NAME) String key, Object... params) { - return CommonBundle.message(BUNDLE, key, params); - } -} +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.util; + +import com.intellij.CommonBundle; +import org.jetbrains.annotations.NonNls; +import org.jetbrains.annotations.PropertyKey; + +import java.util.ResourceBundle; + + +public final class SpellCheckerBundle { + @NonNls + private static final String BUNDLE_NAME = "com.intellij.spellchecker.util.SpellCheckerBundle"; + private static final ResourceBundle BUNDLE = ResourceBundle.getBundle(BUNDLE_NAME); + + private SpellCheckerBundle() { + } + + public static String message(@PropertyKey(resourceBundle = BUNDLE_NAME) String key, Object... params) { + return CommonBundle.message(BUNDLE, key, params); + + } +} diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/util/SpellCheckerBundle.properties b/plugins/spellchecker/src/com/intellij/spellchecker/util/SpellCheckerBundle.properties index f751b4a008f2..fc34af4e389a 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/util/SpellCheckerBundle.properties +++ b/plugins/spellchecker/src/com/intellij/spellchecker/util/SpellCheckerBundle.properties @@ -1,28 +1,27 @@ -change.to.0=Change to ''{0}'' -change.to=Change to... -rename.to.0=Rename to ''{0}'' -rename.to=Rename to... -spelling=Spelling -spellchecking.inspection.name=Typo -comments.with.mistakes=Comments with mistakes -word.0.1.is.misspelled=Word ''{0}'' is misspelled -add.0.to.dictionary=Accept ''{0}'' as correct -class.name.with.mistakes=Class name with mistakes -method.name.with.mistakes=Method name with mistakes -field.name.with.mistakes=Field name with mistakes -xml.with.mistakes=XML with mistakes -property.value.with.mistakes=Property value with mistakes -string.value.with.mistakes=Literal expression with mistakes -local.variable.name.with.mistakes=Local variable name with mistakes -doccomment.with.mistakes=Doc comment with mistake -user.dictionary=User Dictionary -ignored.words=Ignored Words -enter.simple.word=Enter simple word: -add.new.word=Add new word -entered.word.0.is.mixed.cased.you.must.enter.simple.word=Entered word {0} is mixed cased. You must enter simple word -entered.word.0.is.correct.you.no.need.to.add.this.in.list=Entered word {0} is correct. You no need to add this in list. -no.suggestions= -process.code=Process code -process.literals=Process literals -process.comments=Process comments - +change.to.0=Typo: Change to ''{0}'' +change.to=Typo: Change to... +rename.to.0=Typo: Rename to ''{0}'' +rename.to=Typo: Rename to... +spelling=Spelling +spellchecking.inspection.name=Typo +comments.with.mistakes=Comments with mistakes +word.0.1.is.misspelled=Typo: In word ''{0}'' +add.0.to.dictionary=Typo: Accept ''{0}'' as correct +class.name.with.mistakes=Class name with mistakes +method.name.with.mistakes=Method name with mistakes +field.name.with.mistakes=Field name with mistakes +xml.with.mistakes=XML with mistakes +property.value.with.mistakes=Property value with mistakes +string.value.with.mistakes=Literal expression with mistakes +local.variable.name.with.mistakes=Local variable name with mistakes +doccomment.with.mistakes=Doc comment with mistake +user.dictionary=User Dictionary +ignored.words=Ignored Words +enter.simple.word=Enter simple word: +add.new.word=Add new word +entered.word.0.is.mixed.cased.you.must.enter.simple.word=Entered word {0} is mixed cased. You must enter simple word +entered.word.0.is.correct.you.no.need.to.add.this.in.list=Entered word {0} is correct. You no need to add this in list. +no.suggestions= +process.code=Process code +process.literals=Process literals +process.comments=Process comments diff --git a/plugins/spellchecker/src/com/intellij/spellchecker/util/Strings.java b/plugins/spellchecker/src/com/intellij/spellchecker/util/Strings.java index 306ff3d17b5f..675c608b6925 100644 --- a/plugins/spellchecker/src/com/intellij/spellchecker/util/Strings.java +++ b/plugins/spellchecker/src/com/intellij/spellchecker/util/Strings.java @@ -1,76 +1,76 @@ -/* - * Copyright 2000-2009 JetBrains s.r.o. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.intellij.spellchecker.util; - -import java.util.List; - -/** - * Text utility. - */ -public final class Strings { - private Strings() { - } - - public static boolean isCapitalized(String word) { - if (word.length() == 0) return false; - - boolean lowCase = true; - for (int i = 1; i < word.length() && lowCase; i++) { - lowCase = Character.isLowerCase(word.charAt(i)); - } - - return Character.isUpperCase(word.charAt(0)) && lowCase; - } - - public static boolean isUpperCase(String word) { - boolean upperCase = true; - for (int i = 0; i < word.length() && upperCase; i++) { - upperCase = Character.isUpperCase(word.charAt(i)); - } - - return upperCase; - } - - public static boolean isMixedCase(String word) { - if (word.length() < 2) return false; - - String tail = word.substring(1); - String lowerCase = tail.toLowerCase(); - return !tail.equals(lowerCase) && !isUpperCase(word); - } - - public static String capitalize(String word) { - if (word.length() == 0) return word; - - StringBuffer buf = new StringBuffer(word); - buf.setCharAt(0, Character.toUpperCase(buf.charAt(0))); - return buf.toString(); - } - - public static void capitalize(List words) { - for (int i = 0; i < words.size(); i++) { - words.set(i, capitalize(words.get(i))); - } - } - - public static void upperCase(List words) { - for (int i = 0; i < words.size(); i++) { - words.set(i, words.get(i).toUpperCase()); - } - } - - -} +/* + * Copyright 2000-2009 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.intellij.spellchecker.util; + +import java.util.List; + +/** + * Text utility. + */ +public final class Strings { + private Strings() { + } + + public static boolean isCapitalized(String word) { + if (word.length() == 0) return false; + + boolean lowCase = true; + for (int i = 1; i < word.length() && lowCase; i++) { + lowCase = Character.isLowerCase(word.charAt(i)); + } + + return Character.isUpperCase(word.charAt(0)) && lowCase; + } + + public static boolean isUpperCase(String word) { + boolean upperCase = true; + for (int i = 0; i < word.length() && upperCase; i++) { + upperCase = Character.isUpperCase(word.charAt(i)); + } + + return upperCase; + } + + public static boolean isMixedCase(String word) { + if (word.length() < 2) return false; + + String tail = word.substring(1); + String lowerCase = tail.toLowerCase(); + return !tail.equals(lowerCase) && !isUpperCase(word); + } + + public static String capitalize(String word) { + if (word.length() == 0) return word; + + StringBuffer buf = new StringBuffer(word); + buf.setCharAt(0, Character.toUpperCase(buf.charAt(0))); + return buf.toString(); + } + + public static void capitalize(List words) { + for (int i = 0; i < words.size(); i++) { + words.set(i, capitalize(words.get(i))); + } + } + + public static void upperCase(List words) { + for (int i = 0; i < words.size(); i++) { + words.set(i, words.get(i).toUpperCase()); + } + } + + +} diff --git a/plugins/spellchecker/testData/inspection/classNameWithMistakes/TestUpgade.java b/plugins/spellchecker/testData/inspection/classNameWithMistakes/TestUpgade.java index 8625f8f02c90..94bdaf78f9f1 100644 --- a/plugins/spellchecker/testData/inspection/classNameWithMistakes/TestUpgade.java +++ b/plugins/spellchecker/testData/inspection/classNameWithMistakes/TestUpgade.java @@ -15,5 +15,5 @@ */ package testData.inspection.classNameWithMistakes; -class TestUpgade { +class TestUpgade { } diff --git a/plugins/spellchecker/testData/inspection/commentsWithMistakes/A.xml b/plugins/spellchecker/testData/inspection/commentsWithMistakes/A.xml index eb2062d35943..fcc28ba38b59 100644 --- a/plugins/spellchecker/testData/inspection/commentsWithMistakes/A.xml +++ b/plugins/spellchecker/testData/inspection/commentsWithMistakes/A.xml @@ -1,6 +1,6 @@ - - tag value + + tag value plain text \ No newline at end of file diff --git a/plugins/spellchecker/testData/inspection/commentsWithMistakes/SPITest1.java b/plugins/spellchecker/testData/inspection/commentsWithMistakes/SPITest1.java index c94fc301ab8b..2b4d1c9cc593 100644 --- a/plugins/spellchecker/testData/inspection/commentsWithMistakes/SPITest1.java +++ b/plugins/spellchecker/testData/inspection/commentsWithMistakes/SPITest1.java @@ -21,13 +21,13 @@ package testData.inspection.commentsWithMistakes.data.java.src; */ class SPITest1 { /* boolean is Java keyword - commment + commment */ - // single line upgade + // single line upgade void method() { /* - werty within method + werty within method */ - // single line newss within method + // single line newss within method } } diff --git a/plugins/spellchecker/testData/inspection/commentsWithMistakes/test.html b/plugins/spellchecker/testData/inspection/commentsWithMistakes/test.html index 0405dd090da1..cd210a55603b 100644 --- a/plugins/spellchecker/testData/inspection/commentsWithMistakes/test.html +++ b/plugins/spellchecker/testData/inspection/commentsWithMistakes/test.html @@ -1,5 +1,5 @@ -

+

\ No newline at end of file diff --git a/plugins/spellchecker/testData/inspection/commentsWithMistakes/test.txt b/plugins/spellchecker/testData/inspection/commentsWithMistakes/test.txt index 74752f5027f3..0f2937c30b4b 100644 --- a/plugins/spellchecker/testData/inspection/commentsWithMistakes/test.txt +++ b/plugins/spellchecker/testData/inspection/commentsWithMistakes/test.txt @@ -1 +1 @@ -simple ttest file (just plain text) \ No newline at end of file +simple ttest file (just plain text) \ No newline at end of file diff --git a/plugins/spellchecker/testData/inspection/docCommentWithMistakes/SPITest6.java b/plugins/spellchecker/testData/inspection/docCommentWithMistakes/SPITest6.java index 59875179cf29..0cc7177b26fe 100644 --- a/plugins/spellchecker/testData/inspection/docCommentWithMistakes/SPITest6.java +++ b/plugins/spellchecker/testData/inspection/docCommentWithMistakes/SPITest6.java @@ -16,7 +16,7 @@ package testData.inspection.docCommentWithMistakes.data.java.src; /** - * doc commment + * doc commment * * @author Test Test */ diff --git a/plugins/spellchecker/testData/inspection/fieldNameWithMistakes/SPITest2.java b/plugins/spellchecker/testData/inspection/fieldNameWithMistakes/SPITest2.java index 69f9bc531f86..f9fce20dc048 100644 --- a/plugins/spellchecker/testData/inspection/fieldNameWithMistakes/SPITest2.java +++ b/plugins/spellchecker/testData/inspection/fieldNameWithMistakes/SPITest2.java @@ -16,7 +16,7 @@ package testData.inspection.fieldNameWithMistakes.data.java.src; class SPITest2 { - private static final String TEST_CONASTANT = "Test Constant Value"; - private String ttest; - private String camelCaseTtest; + private static final String TEST_CONASTANT = "Test Constant Value"; + private String ttest; + private String camelCaseTtest; } diff --git a/plugins/spellchecker/testData/inspection/groovy/Test.groovy b/plugins/spellchecker/testData/inspection/groovy/Test.groovy index 6e603abfbc93..c35a6c90fc75 100644 --- a/plugins/spellchecker/testData/inspection/groovy/Test.groovy +++ b/plugins/spellchecker/testData/inspection/groovy/Test.groovy @@ -1,2 +1,2 @@ -def abc(String dddd) { +def abc(String dddd) { } diff --git a/plugins/spellchecker/testData/inspection/js/test.js b/plugins/spellchecker/testData/inspection/js/test.js index ec237b1fa328..5c0f3b3d8c91 100644 --- a/plugins/spellchecker/testData/inspection/js/test.js +++ b/plugins/spellchecker/testData/inspection/js/test.js @@ -14,14 +14,14 @@ * limitations under the License. */ -/*some commments */ -function testFuunction(){ - var upddate = "test variable"; +/*some commments */ +function testFuunction(){ + var upddate = "test variable"; } var obj = { }; -obj.ttest = function(){ +obj.ttest = function(){ }; \ No newline at end of file diff --git a/plugins/spellchecker/testData/inspection/localVariableNameWithMistakes/SPITest3.java b/plugins/spellchecker/testData/inspection/localVariableNameWithMistakes/SPITest3.java index 2cad16df18d4..1310c707a983 100644 --- a/plugins/spellchecker/testData/inspection/localVariableNameWithMistakes/SPITest3.java +++ b/plugins/spellchecker/testData/inspection/localVariableNameWithMistakes/SPITest3.java @@ -17,7 +17,7 @@ package testData.inspection.localVariableNameWithMistakes.data.java.src; class SPITest3 { public void method() { - String camelCaseTtest = "she is reading"; - String ttest = "she is reading"; + String camelCaseTtest = "she is reading"; + String ttest = "she is reading"; } } diff --git a/plugins/spellchecker/testData/inspection/methodNameWithMistakes/SPITest4.java b/plugins/spellchecker/testData/inspection/methodNameWithMistakes/SPITest4.java index 0dbc4570fe52..fe532e4f84a0 100644 --- a/plugins/spellchecker/testData/inspection/methodNameWithMistakes/SPITest4.java +++ b/plugins/spellchecker/testData/inspection/methodNameWithMistakes/SPITest4.java @@ -16,8 +16,8 @@ package testData.inspection.methodNameWithMistakes.data.java.src; class SPITest4 { - public void methodTtestWithMistake() { + public void methodTtestWithMistake() { } - public void methad() { + public void methad() { } } diff --git a/plugins/spellchecker/testData/inspection/php/test.php b/plugins/spellchecker/testData/inspection/php/test.php index e6a783f1b8eb..f9cf9ab916b1 100644 --- a/plugins/spellchecker/testData/inspection/php/test.php +++ b/plugins/spellchecker/testData/inspection/php/test.php @@ -1,5 +1,5 @@ Classs*/ TestClasss { +class /*TestClasss*/ TestClasss { } ?> \ No newline at end of file diff --git a/plugins/spellchecker/testData/inspection/stringWithMistakes/SPITest5.java b/plugins/spellchecker/testData/inspection/stringWithMistakes/SPITest5.java index 58c6264b7999..9c6ba502c87c 100644 --- a/plugins/spellchecker/testData/inspection/stringWithMistakes/SPITest5.java +++ b/plugins/spellchecker/testData/inspection/stringWithMistakes/SPITest5.java @@ -16,5 +16,5 @@ package testData.inspection.stringWithMistakes.data.java.src; class SPITest5 { - public final static String test = "upgrdae"; + public final static String test = "upgrdae"; } diff --git a/plugins/spellchecker/testData/inspection/xmlWithMistakes/test.jsp b/plugins/spellchecker/testData/inspection/xmlWithMistakes/test.jsp index bedf628bc5e9..45a958f73c20 100644 --- a/plugins/spellchecker/testData/inspection/xmlWithMistakes/test.jsp +++ b/plugins/spellchecker/testData/inspection/xmlWithMistakes/test.jsp @@ -17,10 +17,10 @@ --%> - Test <TYPO descr="Word 'ttitle' is misspelled">ttitle</TYPO> + Test <TYPO descr="Typo: In word 'ttitle'">ttitle</TYPO> -
ttitle">Test ccontent goes here
+
ttitle">Test ccontent goes here
test link \ No newline at end of file diff --git a/plugins/spellchecker/testData/inspection/xmlWithMistakes/test.xml b/plugins/spellchecker/testData/inspection/xmlWithMistakes/test.xml index ce57a68e25c4..28192c0fde34 100644 --- a/plugins/spellchecker/testData/inspection/xmlWithMistakes/test.xml +++ b/plugins/spellchecker/testData/inspection/xmlWithMistakes/test.xml @@ -1,3 +1,3 @@ -ttest attribute"> - plain text gooes here +ttest attribute"> + plain text gooes here \ No newline at end of file diff --git a/plugins/spellchecker/testSrc/com/intellij/spellchecker/inspector/SplitterTest.java b/plugins/spellchecker/testSrc/com/intellij/spellchecker/inspector/SplitterTest.java index 1e339099fd77..57843e55cf9d 100644 --- a/plugins/spellchecker/testSrc/com/intellij/spellchecker/inspector/SplitterTest.java +++ b/plugins/spellchecker/testSrc/com/intellij/spellchecker/inspector/SplitterTest.java @@ -15,8 +15,8 @@ */ package com.intellij.spellchecker.inspector; -import com.intellij.spellchecker.CheckArea; -import com.intellij.spellchecker.TextSplitter; +import com.intellij.spellchecker.inspections.CheckArea; +import com.intellij.spellchecker.inspections.TextSplitter; import junit.framework.Assert; import junit.framework.TestCase; import org.jetbrains.annotations.NotNull; @@ -80,7 +80,20 @@ public class SplitterTest extends TestCase { correctIgnored(checkAreas, text, new String[]{}); } - + + public void testWordWithApostrophe4() { + String text = "we'll"; + List checkAreas = TextSplitter.splitText(text); + correctListToCheck(checkAreas, text, new String[]{"we'll"}); + correctIgnored(checkAreas, text, new String[]{}); + } + + public void testWordWithApostrophe5() { + String text = "I'm you're we'll"; + List checkAreas = TextSplitter.splitText(text); + correctListToCheck(checkAreas, text, new String[]{"you're","we'll"}); + correctIgnored(checkAreas, text, new String[]{"I'm"}); + } public void testConstantName() { String text = "TEST_CONSTANT";