qi>C<3Bxvs}3B$`;t}0vOV~;Tm zk%*m>6xy~+YTBfP`edm=(khcebTWi|uv&f7S;)=eu8D9L_San3 K(>o<2B|UvAS3h; zsTtY;$O2&oW>jDaxM)6qHY)K|epRDfs@;3#1<0d`khETKvfL=m3P^C=(|WCJ9O52d z;JtU)@#AeyJ|zK`S={X^66a)d5q7Q^w}^Ah>vU9e9d0|he!3j+t=T2_=c%T1+aks+ zToQp}54FtQ8*mkP2^K7f3Xe_@uWfMS$&l>Vo^AF_Dd4n_HbWL3=_$KSc-d*Q@Ft{K zqj+5QW3AdZ$5rBCzqlYxwmdxQG{;OK;r)hTHSSGrt4=ysH>5^yx?@hC+4}Fz;5n$x z0ldjw`~@=msuo}TlWSnR>*uY6JOqLT5ov+Q=P@p`rMKZ>&g!>9ce4r8nZOQDXCen2 zr7?`sp+oyb;o3R74gW$v$&6G0Yq0A>o~fatz;!F|#jw^8jJ!L5G?#OoqMVaJU~o_I zo92+ylgqDq$2URQ&6c4MhQwHtb}F?AWxK~~nc_x1UB+r2e=-YAR;v#esD`3%Z3aLA z!FCs6GIDmAez*=Eu*&-l7F#9YEi*SI&7v`!06UhTB|$NZ8|o{ZJeM)2@{JV~SfmB! z2DzkwL`B@Rd#b-1g5z^&%6cV_K`%9J46i6t3@JJTzNO1fC;E;#RE IsrMIH5 2tb z045QkW>y~IVk&v@o`sZ_LjsdlZm;9waMR17{T&N#T%E*p%?&97#&Rd{pO8&qF i%rl+_}W>YkNI(`@uddrq6?u6qmJXqh+Vt!n$IR z@ X^Q&us4qf2X^rl8oy)cTX?1=d`&lCnEdxqutb>cJ zoWRn1u%=AsCH@Ft8{l=d)hJw3HNcig+#q0CEqKWmLc{xTG;_y8-mJ}Gc^&*Y-;ugA zSZeW{!lw*Dh90oT1T?o6RrP(^s+;YJ{Czqzj40eLXPb27dV8upzb+b=akAbq*Z7i9 z`4)yk)Y{#{;|U-u(b5fOR_&2i9vo*QC;;VQQ}k>eG~+|Oss=E%b1=hY?`JjVdZUZ9 zci`L9FTA}Mx5yBZMQXs4ST7H&ygC3izcUGQ*68{?%aVK?@?sf@_A96J8Cc<~go$@S z`;G5o)_i_D-z#43*-vv|s-75GMspP7B2HZHq9`QUre4#Tf(wG2tyB(eY @o=y}RU0to-IsX^S$Cujc&dAO(_mt&xUCw3Q*N2H~qyH)0tLe7)-L3vG z$TC6?DQVurf|@BV{4(P-%s$9C;8fy@c0cx$B#v8{C~XFH26DxgWLeZkzHQjGf+p#l zcs#_yWo{oft?v0Buih@7m;1`l9r0aim%>F8n83W79{iO$-`^brTuE~rX1H1wAzq4H{+>nUsKvQ9ww`rqCDr*KDg+VWNoyU#YO0c*-s?qtfA zK8%kdfP+mlOKemVFBd~C_G#YbYMt SXAHY{LS2TB=}~O#&B9w7c~^h#TwVU8RehRVjyI#aL3|jT7Z6s-UF=z 4a?TnRy(%z^Zr;)uwcVAoHMtyel LKfV z%+dcUU}6fA;a;O&3;5!b*|lB_Snkm@x#xsy8InQMBVR;fhC?FT!e@OLVC|41XId_a z6mf}Yd@hk~P0Y=m$H7cjCXzjKF8nPBu15-%LTAE4oaB@ihQoI*C+FKvv4qvhu?94I zabA%Z&FUqNT7voE#(W99T+YCnbG7@Y 19IftbD{Q_?N~% Y(md5J>5@ZJ)93EY0& zW&q~qUUprDCl;ReCcni8+t)w=x4`U;BwPG4PACly?^xM|qX|Zjbu|`OO-y1))Bjo{ zbOj$0N|A$1f12A71Ea>8U|g8;8sXuAGEV`g<&x(Povu$i$1#KBPer`hb@p1wZCIIC zD$bhbDH^}3uKnXVf+aw`ShTB=uP=sNJ!?!A=htMqXdJ>XrRse%EJlhlF;Tme=vXRB zG>DiUAsk2{n0SSZhASwLAubuj_JI7uz2a@kMj<}_XkV6i_j&f8^Ph*>i~M=bY6w)W zD_7{%@tlN-RctHR+~Nr~2dbu6)iRFC)X#aFQ)LJZ6Wk9zU%jBLD&Mf8o~6%ovD*)! z+b{C@P {i&Pf9)>?;UQNI_{7}Y74hpTH(R$8D{W>32AM;C9zlkYuc@=T@9J* z3YdwvZ}3$6*MZ`e$u (h}YaYam(n)t-EK_Z6s4f1hkw zJp^i3Po_JPV?BL-^c9aV|EZtl(d5PVeHFaLU+wU|qRaRF)tT_?q+E89ot2edW-Pf$ z*G)8pVr=g1DCb(*?DF(Olgahu+5QKE_l<_@$N1@f-Qt}5E@1w>tB0E34S(l~zkB!7 zin7IG|D44LncNAv>dw_?MfOEfeD~>F*3>vVA)MD1j4h@4nHwiaJCg31zi;L9Oxo$_ z?i-y1+!#RIXR9CQ1qz{07{ED338p}WkWxJdDqFr!2!^l=WYJ)Xn$wo&HzlR?%)P0= zfDGw87c}&3*}WwcrKK79+a7Nce%-^PkX_HVH+ttQAJ7(uI+HrceP`Vg4k+t-1zk?F z8T6e=PWw{Y1HDe$Q7o9>Bhj^C9%b6&IE{W_W^#rnL8)pBK<<%n%NHf5K{>faZEQyq zhd%nB2$M=7mjTNI18X>r25A*i#IfXMQNP>rJ{R2CQ#5qEpKG=w^&H}!`WfEuQ`Nfw z9^oy32|(CT6UNsSItUqUK)v(q(oYWF42R>~dpK2c=-TxF<1|z^vytv<>wU5`=2*LM6k(RV-R{1 zlNG@xT!Exk2Cfuu^A$eMY^3D&)?f_T@ky}0fjzp1-}HXxy1jk(Z#n+l=bV}fw+}-W z&-K{93Z_>M_gypgv9>$vuLa8-;xRvAHYQg+v96a{+a#=`N%CM6Nq9Bp#=Wc-h?WcX zMpg4 PpHD*X61-J90_*DuMiyfwE$yg-6KXM0=v9vB9?c)EswPWp6iK!t;d`oF67) z69LJoA%z#)`Pz!p%FTcd1D;;~ZcV)is^)oFG34+cb=?^)Z$=~R=GWw%DB`87A5YOW z2t-jOO0P|Hl0nBBw#%cg_0&x>AUuEQ(K%4QA3YAgbwg}PZr?fbEBfX1-^`-gVm?M5 zH>;J}(R|sZ^<^K6P5nJ@aGl%P5t#-Vn7C{b5{-a__wb<&4qp(kYF8(MZfA&u|JIT- zOQa8<9ft|FF3jO6x; Ml#2+Am z>#H$VsXS6Iq7by{$w$IA_{JC6U9P_1o_fjBfd>z;#Mz^t5IGTR(`}#wBhgSM1?KNv znB!L?A9E^T*Mh4V$$rv~M2X>G1CI3cw$$=n?Cisl4_|V}|FN5kMz@FgTZ->v4MI0? zF8W#-h5zbwQ^e=ign`kfjrW;U{-#@~2DMZsv+2tY2!#%g8MNG?vySPyhGMA|r&>(k ze9)K(Ve3TP-$VM(!pL7zffMNQ3K1OtMZwU6+8Fmff_w-lh04kG0gmKh+b0Ge 9tQ|2)Wx$xlto z58sMt-zqeaqd|af3gH?x2tOVEKwlqRG6Vg9dWmZPfX;7k(}o546|*C-d|e0KM0`tI zf!^=47xA_iQ|>YusoR6|$At`vk+_|{dZ0mu7#B)lBii?wEc%Mu5~pc3l!}4`$(*?# z G>Ii)uAF2XxwJ9TZ(9e^0R;X2m>=BUu*Lm-!(WejBaY1SUoT|qQxb*a$I9)2#Gdi zG@{%Sc__SO!%ecwSkX!X1sf`qxLU-Z+exYXPAF}xh-QmsL4@3f`<`z{9{K(_cSV30 zdqg&TPqd$lKziqT4!d4qj2gORr(@+ggB2CQ=DjALGs4Hz#7B+`F)CckZP>qg6vYMa z^!5P;hT`q7qIcRxcZA0n&~EI-b|umHu~hjWdfp^{&i|-HL$I-)d%*wGOt)h2`xYfe zV}y2r{hQ;fX4@?+q0bLP5|gngrneqfN5f_39BrU5Fwm`Bsnho4><$jGKi4_^?$md_ zzI1fC>-N{dM?JV*fQ&n?`}K5UZLH=Q!A^mojYYllLm4_pdRt#1ho1$tZAlNTwVWH+ zA$5=u3Z3F=@&nX0@DaiU?32(dI2D=hJ*aORjL?ha{TCIYM<2BO}iBU%zC_EeL1o z`&EI!sJZ3t5Df}|7r56#g+<92Tg 2!2nl@}v^Y0U9+zxLqE8sFuvvCw;Jp}RiONlpGc}GCPsSxq3<$8SwqHlF zuL<|W0EY$~D-z5#HyxMpf?cILTFmKglke?0xrlV=Ds)qo?U~!G=w#E+ }qZg*SnDVM`W3lSTSo)bekiwrHzm;_~h(E~0Ol}2cel~Jx zGa*B#4uxv@)T-%Jov@jF%@M ttszP}spL6leM)VJE*iG0pe$&M<&Ai0o|pb2?N)W-%Ilyx^;q zuTiFbx-9^nugmv=Xs-D(NSF{;Z{<9>yscXi <`=CYCRk+BF;K2j;EX#Sa$!fX)4imr?Go`={?OJ11HEYZ3_Z9I?;kyY0d9nd z!UYTn5O1)sudq(mv*>3}QB!iuvVut1Bw}V#W@eMFDL+?U&>=>{voyttGLX#&i^MtZ zMwzU@Wu~q<&*Ls20B|)@nWi*D!$RtjNAKaLSVB18DCJE!kS%lT ZTHj0Vj_aM0AxV<)^za+rzl-SA7m(Lh9w@GWg}Hk*gQ89iq)Vys7A;9`;$X zQ!J <#S=ua`{1sI9r&`8h%A|{rsU(`H+4ZwAl$dvaseBM!dfh_mj zj)eqolI8at-b8TU=I}@2jfDuT;k0T-%%{OG&781XTM2#Da>j18d_cVv@cI=)QIbQD z<@blJ?Bu%?j8%ei7+{Jc-4PMC?GRx`V(REXxpcN)M1v~0FW8I1_OT L+!6A9$oIg-(K&sQ78P+?TGNm*~-RCKm;I zx7g=tVNk0qc%v~xO3Yjol>B UQVo3ENE09QCfoWmG`4)@*0Z8VNQMxWZ2b(lb|d=GblAZay^R aL=X&g6FtS2O6 zq-i;UY$hh7A|ZUMH5mw&o|1p3MOpG-^_kb@MOsJriO_f$ItKM=GDI{$G^(!h@qt;> z29%SNQ1JEuDw3M5ixOEteC5SGUbJ+D6zMyAFs54ll-yc^6V*`K w8&==1hJrs?o=FY`PnW6NArDG)9 zH`AFTj!K IrOTPbI$mgJ7aHbZm<)kVT+o}nD3poR(&meqHf1%j zO4Y2&6sI(fNCyh6g1Ra8IZP Ictlj*l-T7#W?dS61s%#Xifnf4uD~7%^FBv#-_l*9q(`bHf^NZ- z3W8jXz5H#vwoPv+a~;nXYT=urFh{D;Ns)AVCu%P&$SBb&kV6W4I$j|kv9o%d(ZrnJ zHJ#1l9~~egZh{ZBm=GDUmuwlwxuZ_!a__TL!qAowI2Ik)xh4AXV}YE)4(>R@Ba^ zr`iE!N)XXR6=`|0%6#;^V6GFM)%5|WG!D 64+VdUf7#+L-o`&>J-t z#N44H2m;~XGiH-Qpv-cj*-|tJqR(6^#U^?{j>DCuAj4ey&x;||+T5~A;@j6ot`EnN zFpjiZZH|jjwr0{URLrYzd!shRQijlqdUD+*Nyt;Pc7gNCM`Ha!JO=o3gMF1TCtvZx z2B@JhZx*EDpp?QuZgnH41qmBxjX3%HR+@3Q#dQOeTx4Ctmh%`|K>;wsKAo+41!tAn zK`i3Xxpht|wvKef>&R!!dmAd1CVE8Pi|wI3*s|GC{T6@~a#B~zO(h2*McHF;CM7_s zHr!AwCCZ>!R_{qKQhcdE;kF`FwHBwy6D3g$+^|lhv%qgiGDW(mdep66s#)2}wLMO? zSIiymD!g=sP$gNrm?k~9muR Rmz96&&Y$dj%8YlWN1k*3~+R-Ce6 zAUKl!$3&OHKT?lse+n7`;m~M53 QhYFu2+T> z7_ Bln$Roc}uy{5G6)nWLSx%-J5XLcj zXiO+UzNb~h^nXY@hD%pQccv)S+9{vAC}AE|I4+@eZM`udL5kJ|vf^x0z7=MU7qqIB zuag2t2>?ZT$u(-Rj+pRM6)8EHr{$UD6Jg9N5Z-I)&y-mX5AyyAuJN>=<>E69MiM(+ z$sS_>$Z0wx^Qj~d;7!rGnj5Uwgyu1Rn9Cm@+%1LKv7`>Bm#^q)KX5t|h2X0-yfoRj zU$?4m?jRuLt D7Vup@mX@RL(4~W z7Wg>`Q_q0g9jV(Rhvr_$&BC#8$5N}ueZoqya{Lqy)*xYXvGYhV&LpwT%j*p%mW;=X zXn#H!B0i7b!{SPS8C*i>nt!7|%+DWHKp7Q@lQ$fR-fQAzn@e)FhVrz=8<(<6V3q zeh|zHFV93{uXpm*(+lt?*?V_myCuu8hAoCm1%Kw_h!#fQx^bqng$U+pZlIq(5Rms% zNmvf5%*v^3(#cwl5_xv-)q#YwF@h}jId1ae3EJDv?B
>AhR1FXFC2dU^C9|My)N{zsvmhbLab34kb~?mOOKirsD5D8k%t zGVY6MLBl#7i9!nk8|_C3b6rjEq!U8~mT(U1dd}K8WjIs#a42l1 HI3*vGvNqELR?vtm=Qu>2-wMJyYi>@1sek#FOnt+{^7w6_ zJTpI?`N;q}Zm^Y1EOIR; !*wF-25WV36N(nv_$aY2vd& z`@C!(*5HMhRr8K2CmeUJYr#xuW8D!^fucqi*+F<3c)ZiRGOli76C{=}U7h2yP4%*- zhl2J% _Ybv9;U Z%>E%~ET<98hZQAMj3SWI)=x6K!i$m&TZLzt%c%ZG9 zs1-QJ`z`*TvPgj`44B1*!iak{q&QXrr qA@uh0Uv97Di2&H7m_2p!rA;vZ>Br$
y)=WADC^9QV$NZO-P9cKd4GyRg=IRe?4*a#TjmIs|hE3cRYDYh{L)9r`>|fe!b* z9H(z-BkxCT2ZQH6M}5M!B8Sj?yq_vLm)!&$b~#hQ-)LduIRT4#e8uIvZVB?KuQ_*# zbJt+#T<6Ax0xI6CrDe4*uc8;1gk$Lv>2gX-m2uMfg{)2wqv9S}wE}bao!4 KBK@-Dx zNemNURa1(lEPyFa_XlwnoT>q6fh FQcfKF>fARhPxn)c5>}u&^O7CXr Pckbh@tKD$#BwEv*l7){_}fh(|?^abN|^*{lb?^gzwO+G-k1$uKY zWUU%B&`AQF>F~T@tw0~9B?TTFB3m8F?@N)qO@q^pQ)Id}HHFbidfqxphn~!RI#IJI zB~V!^0ZkYetTZr*+=!+; VpHQ_WlP;b^X3RNn|RlAum0ttJfyIcDK-Q&7zq+x zuyJ4|V3#Hs&2|f*oZkV=7|ox#WHAOPP1vHlk{pgpmK;=)zD;PJ0~%sn3*I9zUe~?r z17i^&L>HEhpoxx9Y7(nZI+{wmmZ_XlJTjTMnEE4;15_;wG;p-+{o#Z$3m_?j-q$eV zqU }_Ey|;E+V4Bac{`4gi_Ig0goj#n*e~#|RHNHy?b<_XC-um0 zL!P8M7Y^PZD1I^}k^vIk7MkyA6Hg3bdCD;|ilU^cc0H)4GV@@APmg9)QgR~NKU<+` zlyXYZ8v0$Ijg+)7Igh)+P}Fy94z6j>$Q2J4E0rA&K5RDxGt_q6Kb~gFMo_Gxu+0!) zpK`{pP*9eiGlxe8X)K9#g>xSa39_qDLE}X7$}OmpFcp9iV*zhOg@Q;yvJ4wW$i$e` zfhHo=B1cep7x9(ofG2hDZJszv;P$Oyn`dBPisSGHt~-Uhwm)jTKnq0#Naw4Px4RCp zU&q?|aDJX1-%i|T^R8s;u4DZB7&+exwUmn&y|~YT(k1SY12~{F`ubO+xGtnz6A+29 zOHE?s&Cpt!ag)19gshhD=4;0X=#yvh!r^bi^{+$yd4rarZ{-n{)pIiFT*p17U*=p} z>JTohM)ri%vX!gw$}dwiZ>ZFD*~rIDtVOVEvw#RPmumSAhtpjI_(^9by(JMRwufsC zJ$!2s;t8+tFcZ$MGFC}%oY0A>W$DA|MIN;7pr3qW<_o6WfO%-4BlCjSn=R9N4!uaH zUT}hX+(w5YO+qATrGFYdr-0pDcI994dHlQa{eGU0Hu(p$+lJ`f2LJG*GXf1<+W3B@ zVN?_`dUkU6IwRatLVli1*%|Jc6%iI|S#btOY%-LaNaQeMx|rpBy=B-)9mUw=$&hJ5 zIk)QFU*37EGl m{f zeYD*7Tso^YJGPg^E@YKWoEVxuR)AU1)Bs~K(+1p~9K;be)dWc{vREm^Hmz0k3QSKe zO zNj|cYRE0@so9HNysG=on`6Npld5rkvy4Ex(Bp_5g4?sOZBhq1M!2>eXNPOX@jr;*l zJnmkS0;w3a%v|T6UQY8Qxhx2Yn6%z=7&x`Ou<_y5LFNNmOmL>W0fdaqNf5g&fQ{|6 zyrE?B{+oPWq+Z8bFgxJvvPS!os7xmUCxbwLavVB1fb<+7?VNrGjt~~OVU __g3MJLM?Gn5?oR!4YecB;x?I^2XtSQl(KVj5kN zaw-X7z&|=ybuBDAn0fQW=x R&MUk1)pI07s`8bYJ#3PL4a zVdgCu@dUs!Yw0HWp*hf8A;TrKayUHEhLap=ZvB}m#G;cK@g9j7i0J9r5l59A#ASL| zSiEoAY#*T1yQ!!@)qQgN!7e$xHFZ1 5NCCZvs^{6@3bhk$BW1*-OQ%1<{#R>n z8I|XjY>ndX?(PJ4cM0z9?(PzTySux)ySqCCclTfc0$kGPOLz7@-F;=;;s@}K!KBvv z42oJMbHcZ$P!) I6%knAb`wt_-&NPCN&~RGZZ>rsi4f&pE8hiqrjk|LG2=0# zC>Hw0x(G;ze{mWWNh?$lO}#C&JJL~*Jbu=}INb2>JUxjm-jEw$V!~I=l&i|4=FtwV zKiv}41Q@6bFKX1xt<)2tDgn+{kQesaQ%Ps)!UaQB8igoC3%%$H*FZJS=qDU7$N0`k z*>7#1unrg+w+Cor+n2{5S)9&BV;>%iW0<19=-TmG$Drva)JW)m+*2;$Qk-Km J2RA8LQdVky!RDgcMsVqZ zpR@Q%u6uJ+y TU%se+u|Nh%-EEDUj-*VlxYz+DlJIXR1k&XZ-;PB z+RfwG7l`331Y_c4ezkr50;I7$`flY*^yl9wsT|GsDm I>evb;H^%9osCIXM<9>NKJ(Jb#`eA*$BCFc{<;&~O z^2q}>H>)npfXzX|*0U!^5@K>9y7_y*ZR>@)p?m8UC%rLu>gw5}8fb{}*0;0qv#TzO zlDfw8l$C;o&sL|S^oyRhg<8zHg_!Gpdk@6wx=lV9v0qdze-8QlcosQH?;Yp+^6A^t z+tHaW?faMKn|{_RnrY8Mq5*vV_rwjx$1#Sr=9PwPp1o;b$jV27B38v*JCUgP2$y;^ zr*o=u;fv3s@pT4p;pAam#_%4U%LMYJ4}+!6TD$Q-di`C4f3SxQv-{iZ)4cy 9ox9~#s-@?nk#=ZYf&Q+*-W`it-@;0p~wGMR!Wr_?`rc?|z%TKycBuBd| zh7ve>$&T5vscm9nJ*PL?N%%3j3MHYLJJ*oQPmNfnqeM!8S5$G)Au@e_zUU%oMniqZ z^EmE0B?ZMx9s(^YEuXSJ-vcu_nHr%duvr)-k_oCLkfv6EbFmS=KYzS;cKqGAATy<# zUrL_>jx(a6>?*~tE$T2yt~0>mI&)_BJ01wWhKPnWkTMnOJ{MZ4La-WV9Ewsp45<+g z+76i<(Kxhlh8ZV$>KP(gQWQ|Myljh9{LuaAz@>NznGn@i2|#% IBtjj81_H-y1jy KEFQm} z210W@N+xE*eF&xc$$da=fO~I-zScVU7_h>C)jw4q*;L@uRFQZHA0icl zKn?Lsnb?5zd|A5+D+4A~j8W%KY$_8ECfKq>=IAHP9VqYdR0;zwV$}T~O_iqM3ZMPr zrzAswjJ=TXu5vUISUN7&ilJ1~bL;hr;&x@Owye^ia!CyRq5S-^L>bvZ)$P4=?7~t} zDhn+Mn7lAXNFhbwh+~&1Gs4Wk_W1)00op^L13180#95&YAP)0nF=wH|v}cf?z}U3I zt*EQNVY)Zo96tDq5*c9_ULQ>0D6RvEJ7z=Wp>i$bfp>45{B-YZ{zC3%_jquqI~a$P zH r~aq^;kt2|K7ky zBf5wh5dKZK#=gDzE$4BsIure33O{`t>6UbOf+Gn(5vi3Vq@Prd+MXRUbUc*@t=oLi z$Y;v^twZ{WNFQv^VBsF5ay}Z8+9a(q%({hzrndNKmMe}PW-Pj=fm!?=tVddki2+MZ z0`OB~MCpQT1<}(yoIi}as&x1n^jixPpP*t0bJgXwjYQy2UGwDH$McShAF=>%* zme4t=9{U|00Pfl%1xcpX^>DdWWP9VtmLWx6!vVy>Ia(uGh584)#?h$0Z@1@#iaX+6 zeH8XG$b@>>*SMgh5q#f!8=Z8eK$xhx*Ogj1iJCd(VgNspY)FSm;m$wokRd4QEoQ22 z2;0(%HK3iIoj(DdmpC$Gu2s_6xMcI4@Ty-_Y01Ygu{A+)$F59gfm=R$o~8FzRyVVD zpYW(Fo_bTM?~l*`ZbT)AGN8Vt48cipDfYyISlgv69Nc~r!Cy;LNFLVq*9b_IlXrob zm;ac82L(b8BHUvi=;J=z|5V@GlZSvu7age|jWw1saI0qcehJfCM?N> ZhfHOyMUSZn)jZ=r`s1EC~=JOPN zHt?lHNtm^ai*ZOSS^p-7MX1O%uTwLHo7JmBe)C(X-@wv^t>@X<8jwGyU5UIU)sPt8 zt9|x(|8oZZa7iw~$}j{rvv`n8zqWo1-1{Xxd*~US)@rx~0%x;In q;>o(U5p0g!Cy4ib|`=WoA>x;92mWj*JVocZImMYFYSd1s+| zSkq;SmG?d_`zNMv1k1W!$D+U3wpQq_JfHT$@ye;3-Xpgz!!6+v5$dxod_KB_&*V}y zrsWu2cD;Mt)7v}Bc|AwfQK%hVk7Z Cp%F_@4=$ z?FVzeS5N=|osTJ=|GYf(zi;xtHr!myja?n-4D_80%@m9sovoZ4RX$$%$X*v!H*D8f z5qzd~?Qgz{3c|LI8* {}*zR(v$ z2 *DTq!=$wZWj0m$vR zl_bknqL#Zg*tH`^KZ_Dfn5+PVfDhs5>6jW8kZ0-!2xHQazokijB9KpJ47O_P!0QKN zh1(c sOha)4o8)g)KIE1$C;tlk!p5`a8Be{QyZ!m6!$JPx8EvCI<7D;5Sj^L z6>?M_ms4p?P7A^i}LidffGsknuqCoa;Ai7POr^j}D;}Xh`2&TIo5YY)FU7q8LHw zPFi{m`UA=cgQOx>PYTu43`Wetl}I=XA(PMN`uFi-#ib%F;$(kaii3M@2==ddAov zL6{{C#y_R`bTR|i3=lq-PpDDKx1P7($}u~@k;f#PV?E2HTJ>Tc2yHjqT1fHn_0~0| zsnF?i4$~C>OdF6|?SO92q_IN?0>gHX!RSL9eWkjN4t~Tr3ehaK?)y4*hYq}%rlh2V zRGDq1yuhLd)t4mTP0wk{spHiEwLl(49j9~yB8{Z%N4_5jY@sP3RA>-OVEytW)ACN$ zI-s{b0z41B{*+4oBXVkPnh)vf!HxNJ4qQ`)iprwv3{Hr-?D3*EffCfSwtc^L&ImJI zrc2K~E>G!;LF=jWlyYXCeM3-q$YlAJZK!eq>RV`iSOYdlZ<%*@^_ZM^f@_iTv9;*E zCkd1yLRlBKlvsNmTfD{Yx52g5x_kY*p-$~tK4zMOh%nA``Xmv-Qp>#Ucn`t@bnd4R zhVM{x@{}(AfQo0_MXL_u2xNS&t8p6kPzw(r8~*B|m**~Y*i53t{fN|IPSX6*GMBjY z@R|%&T!chMrYx#8dl7c))P|yqZ5^`T?{xJgK!Zk+`I#%)#Y@^O!AsKK(u%8!WvjD+ z=7snDrjI-kR)bV6_d7S=qG+-$o{r9%L+MKKPY-XvNcO=aZCOmFS@mW6BqxLe;hB24 zWvlXa1YVsF_+(vHUF+uAFQp`!qFU;@vz`_}Cbd_q&af{vFB@G8EvKC*LpaVu)}~Ff zu7VHK2~S*O-QVDQcd1Oi$MWp=F OeQcvdf z0eC(fIY$1L7liN$c3S J5{D zG@~4X6(j~}0^fw5aWRYAKn1qan8J 5eI`cKxe>xrv0Ks2m?R?no)hY*Am#cTTG&VO_<*Rpt}K>nr1>L%rLWt6=Yf zn_gZCBQ#s&-3{2PtEK##Ub8!33i&Y%I&+2m>PkM|H`Y{VBT-$ei}!!``PvC_8^d z^8lp(Kccy~leN`HG;dV@s6`B+e$*lu^tD4Qk{7CG!F*jB1L+u$dR$;UwR2=2LU2AC zMvI%6v?N=Bv s8a8L{lcMZWP~<-%Z3Mh=;XVWsJZl znM_%CIi_=12KW&WayS>%GC!)^LfIEw#G&1fa9n~1j7!H`f$%09;9(}yBH9nR3W-Lz zHv}%xLx0BojEA&PPCH2&;F3PFr^cCiD;yvd#%7e93r3}`o#yBzE548yp``(WC9?@f zNnWJC)+nbyW??Z7uu?hWNOIrF$Uqj|tvb7e<2NmLoF3;Cm{bn=F?Ko;;DIMUO^U6& zp!WIO_X<<{WB^mEG;gT4_7WI{V6H?f@;ECIN4Rmgl+@&Y$o{yhlSziUDO2&Hv0Dyn zMfm#p0ZYqJNTIea3D$ziyF&;|0`uf9p>@PfCqtn6`Cgs$4B~adERZE8y^P7%8As{) zPZCW~CMxnYCda$5j`m$Gnj(Bc;n2{;rp~oWKRk_aGXttAS#!c6LfZj79<@95J>S~Y zarIT5J|^uUlcTLD)tl~QU*JrH8G7OFS4|p&q0-A4G&|hgDH4z|9pJTP(2}& U?F>L ztV$aJdaN $7I1Djtkb*RLvr9JoOW0zrI3hSzv4^lo&&bWo zjaBNPsFZ=XlVPG7A2;WZhi>2eea<)D2|T_V-|g}k9*n4@>2w~XseELWADQx3`G868 zAfg_fC_!HaB_CUIk2|OL(fnh+E9$`&FOY*U_c~@&5n%J#Nzd6^)7Oh(@U<-p^UooX z5NRF|p@kSeFpbs~!uY($J|6jSH>uOfcwu-pSFbO(luN^cF1kM{+g{D@W`i|kcxB-U zAyRHp^$LB_W~${#uR)dFROE1#x#yO)RXkej2~jrWRThRNo?1<+@1R~fvrVki0_+dP z;_5ofya$W3)JsVM36s0Wt24aL22O3KI-@vecnpS&?@<|Mw&*uc)X1l>PB@KhL~WDB zD^3Z~g;^uQ%Zn?PBX*m{;@CNq_?VSM;_PyQYGt&=m6I#vgVmmcPXz%8O_Jmeifhg2 zj0R6eG&y|qQSWVecv|eqU5+ y`rc+6TtYjVvspWcd*i?Mv^G@Pq>N| zhtMKf+5%dG^$)6|X6mTPDzSPW(xM)8Y7of;Xy*VOB>|ly?bu >0r|dQESTI%gFMtY+U@dLiI8@(o1{2{M!#_%1}q4wq**60 zcwJ5q=eBzbgFVitlUpFc12my>+(8qF{zurhLBb#aw4cD}dHI|TqS1;b$rjgo-=aNS z7FPtsC=Dr9%JO9eEkS(ydy<85h-o0w@{QF??$*qi@DveOm9j{prfcJD1w0VYv*f-& zbg~v#N;$2rnj&2wiSAqV)qa_i7N-4dB>8+Iq3}UKY$YNz+K}*gTCxyai>adMp*Nc2 zP|_1oE~b&ky+wRLYF1DYwO_XXP4Vr9@SMTWte2Mxq1P`$0~y3>9w==fhtjwaQ;_hx z!$r-)XGm?!IIg<1vo@va3ZI6WWGHk595~_O+$l1G7H;&kjT=mO4!sf3tpu5Ppj6RG zr5E_nnVu51A_36RZ9QH*Vq8hU! !t>F_cM^X zbMnZ~MBGmx(mtKSK8v`6xJpf&K%9Mxu=a8Z?(1V>VLb+268*QvZfv?$uv;kOr+zwb zDxWYh_aW~B=P+J4M|cFoJ4P>N)lRm4Qar! YT**DhppR0N1iQGtfzr)x97T6~ z+Izmw#{Xczrw+~H>M8Nq6uI=f#ZU$R 4|S;MM)ylY Q%-Mi_`A6tJ>51z~Aa$-gtq*pqG_e62cpmiZP)X4H*~lr{3bm9fs? zC@AsXXjiRN =J+W3ADuv_=TUS)q zGkcY2JJ&LpNI4M1Kz}w7(kT;k45et|6A)VGX~9kpz_Gx!l&h8?=d7)NDz&p|Op1d4 z&CCUPNMyXkV(AJ9{D~=wcZi(l{Oc@GWRi5a10mR$DNIdEUx(7ygWOs1zIdYW{s@62 zbKg@I7HXZuGFa7|<=S~n!9;!tJUO3l*jGzd`9F7@n&^?;3+-IT7UWCX{IQX>3gVtF z4hma0wJINwQMx&%`FH!UFL$5jBTaT}10PUdReSy?-x2K;gH8S*%lh#_!SbJPulWC~ zB>L<2>g;50 R0-uzoSmaYtP<+;-&R7kcjVC!K6&5p69|6e2>)m)84np+~uaE-J|e-)|?E zy7?Qp<@dwi1cn!8l>OFQmZl!`uFm9OdfiEt3LIc7fwV=r`VmamThybuJ@+}sTJ(}7 z&1lW^qr(qsXrf>be`rf9LW)2wr1Mv%pv$I(Nik`O9MlblY?Uc;oTMG;RLS6+rbRKv zn~9gV%jbK60Nzh$!Qx%O>16TS3$i9bat-(sBr&NgQ6(Y_ixAh4iwD?Z73^d{U&CG( z)md!U3`Qu_!C$RRN1qoDFHKEf<8S;tN5isuIKOP}xw$YefA<*YeA0LGV&?uad9k`S zKZ)VG8Urbdm6j{c<|I|q<%cPhy!IXohJ0<~>SQ||Z(n>t#X+(CdEm2<`WdF{2Z7(W zWDN`^nbafqt8uHiK5oFEDH`)SETt68OyOqyo^|U`P0yLgjV^d%)2sKB2a=SH7Q&sA z)TJnXwsi&iNgA#~2yweO1f}HAJ{#M+tqusNW%CZtByg#guZPB~BX-=;Eo$?x<_rFn z%>D!qZ$ro1=I{4H<@X!g`=3CFKP6BXq){hR)$5|Pv_prt{tO&fom*^-HkBIw9^0cR zRWf}9y!~Pe2mbO{X)0S`i>IbdVFA6s%)N$@(G_5zS&9Czx8*;FA8keNZ1S~MTUU%h zG4PAHe8p0{{?W30ak5&}idL5fq0Xs|Bj9SiN`co$!*UBBzvtq0-dsC*G0i|c#i+%3 z |mzOA1By=6q-<9YK zjE^}3^h}a85=?iCjIXWcIhpe)-IlQ+`ol2=DM ky0+#glCQk&+dpnv z85{S=G#|~FAJG3_Baeys2i3fV 03+g8964{s{>M}v&S0zAL34U%h zG%1(io89{;E+$K4P)Q{q2@M-%Y7mrC_`t9xi+qWab)astvjW+lfmti5_c|R>3L46d zMAfcSsZ3i>hg^ L0)TV5+ zg_0v`TCMz6`jpU|5fLWaYkh@X0@Njn0@gp$*=C z ^#c|+vSR;-{%7s5YyedYxKq6a;^$e|7 x~3q)ep|3@D+i4CH-W(z)LTNl@rMhofEKU$s)X>V{;TLo zAId-oL`pR0y-R)=m|4yYY@Tgq)EK9XSlmKZwQxB I`WN#~bI#E`J?znuvM5@3~4>jmLlJKwwKv4bj&lFJ;Rpj1#6YDa` zjJ2P(jk${lO6o%MiHYH8{Aap+GAis-J~mE601D3o>wcoRLL~3RAE$nB>>&1e fZXRRA`N+rt+3~D5+(M%{pYp_0ai1XwqPn@Qx l z!Wu+|R!4v~VnTW6hS1`yV&`I8Kt!wkl9fP;W;QdGZv_T4m#7xdpT1WrCPGzUbpwZ3 z22-p6RtnfFEZkQsL1AAG2~t*G`4}q(6fx1)r6H;nkAqlC_Owb&8+ AUA zV`qb|N%s{|ryc9 N7Fk1S#|}y)3>pqLPW4z-1571GLFp1!2HUM0n0r*e $mt!a8;D34b$|UbZQYCA-W&zdN3GjyF__pnK?+Gf^O@@qBv=_w2fHAKrQZf`18Ca zF=gY!1v!kS2T$(Sxt%oz^2hlJ?-7B5S7$txCkucyZrogE-4?#rbYlTjX0y+hY6RWF zO;KX2kVXLv7`s2nMz0rA7_l{kTwT{!nPD1-+sm_VfwnwgLhI0M4(@caaYgHtZ9Lz8 zAG$8j)_dQy*?_Qpuh5AN@Js05H#li?@%%12S7jCOoqaM>@|!LhUdA(dZ8uml#<{)1 zs2e}}wd^+PE=^9|P}!Mn9#gH=&{chVBRr5nm1Pcs_1SS}5_S91nGcjgp9IOdBmc z-gWU0u5E10>VP{X)um3MtlJ6P%trN#?`Rivf+k{G=d;Q}^M)piD8T@w$JzI-Lm(m~ za4PZRx5n_s`wR89A~4+>IGaDXz3u?O-kT_Xw3idLeZSO2v2Cs@j_ul5{qa5?@D9zn zLz!#mJt!1Bv%VR&qXO<4$xn5U;1`@EtIsyav{(M4#qLWB_fH+RoSn|e>#69S>c_rd zSu$v0KX#AV1HY?VBBo7^T5BO_$$Mpb(RBk|9k6K{Ji4x4ab; cc=Jb2gveOF9a0(}L@<1N?D~8tahX?H!Rd6ni`C`J7s<_i zrqRv1d@!~h88GIdZfdMNt>T2ANOyUStv3f*IYfDa3%H!EV&0!JCp|D%k0KVd4$0Ev zU1^^|IK(FCBiZ Kwf26M95H=hvPaq!*ZsPy7bVu=TQ`vuvRsQeZzOuFLJUzl&m##fElC;{0UI-nv zK2R?>h+#-Fd`VeMvKALzD6UnpHp}x?YxwPu3JFPGLzQ>eQPz>yk&H#SUS@1*`UKok zUr#iCsld*4qf)98wK##D>RexcaF3zaFkWA^E`pYpj^U|+tuGTqQlmuX0@Vy+0^ 0xIFkp*Cu9ptqs-Mohi>|q z$W7nIgGyr~1z@-YTh2bAbo3UMKsO(GUD-SZ6YIE6yp`F;xz~JdU< JH{a`GPg`GR#^_9g!}Wtp6Sx!Y9IhmNCw!Izp|8858IR5r9=`^D z#=q1~3EJ^uc cW3))RawcOw}DoPuW?3DzObT(p;$;d#Ri+t3;kj;vw{$zjMdQfY$NuG zGov3wNgHK{o$|A6l)o5(`N3#J5PD2=nYoM8K63K(fuCMn?qy#)o8~s9@I)k|INVOp z{ulI!=HN%9qe$;L7kz%u2R7hMJ4y|UP(oR0)drp(*x3fa4Ur|^nxu8{^Olg%iD?Zi z7_v?79WXxLpsThZC5me e#KP2s@a>pbH{O BS>rBile1{B=wP|(-~-poy# zn%dYs*ui7pbGcyQLSvk0n4)h~O*YpVEE(__zO;I;<+COf*?URZ;#+cZ)igR`NCijg zay+2xWQwjgj*eH^r?cM#$tc?zcowo =G^W%;=FW^HuG7^gG6VG(ig*TtWKjb1P-N#8((wdC=c;@@|<-1+!#Lb07#0< z-fb
>KPWRU7{A>c=c zxB^6iFj~Wys|i2{<}-07eL?ogJ(Bp|o-Ey_TXy4Sbdqt@!E?0ksg1A9pui%s-T0i; zHuY9bUtKGJgG91kID%MVmJfp$IucgIn*ZFzTj4U&TM}}XE)T=AJ3#S3C>?50;Hhz{ zCD7E_nJ >`OvCs`KM)$$nJ{!8e>heoSf6GtPxWof&CXWDc ? q#OI0Gwn?@ ziZ!(r8~`ZpKOJ1bMmYlD2IFh28!7LKE;Bm75MbJ+*)VlP$PFBqn~jRvw|}ENdx+&HBi~|=fXW!#G^eDeiAA^Mo~!j z)Ua(vcSF}1C-W?jcoiV D z3w^d`W($DinL5M3)vaInPaKh^$)YBHWJNj}7j;ihY4bByI<0eK<#V_JF|!kdezxR- zWr*z$*D^@zIl9hXL4PNbeW0}PioklV1UGJe>@}7n9wluF#x6G7w;*cvmN|^u2FPJ& zW~4pzt*z==O}l@FaT@J4zch&IdxhJkJ^H*`Q5srMJUK{QOgyp 3rdNAsz~ zYDJSmVC4!;Q>D3^!&zxJ7H2kqaL01J(jvm|9dEE~DZ(uW2Teo<*=Upw-k@|LjEPq% zQjxivKA?91JtIi{GoA@I4XOe|LGQ7dDE%r5hJg?w6s_cptT?#Hvfel>VvQDwU!fkC zlmOXv4k6i&{u awR~Aqf{7^CQi9_TknP)kn ziJSOHitogPA*ip0b`Q;?x1~rigcx+YVuatSj?k({R;a^-=10QRux9!={i>SYUQdZ< zA|`SZbp)|!NJY-J8|cRk1&k!@V$r=1DIeuAX-Yk7%No+G>k2r=!>rSTSI4NjbiMkO zR5xkZUP2T>s&oe|LMw^dsP9{Ga6s=IUhe7~PlO6nt*emct`RCyaI`z2@<^x4P=i`x zdAay35Yw=0N2`}&hRVi1w(Y#-?xS=V3Py<3s#L|4={0TZvrGmbiO;HZtn*=li)v8{ z8$dh~Py6f0Jt{vblJua}0(~x$c5cRJg|QASmJ+e<&=GdQ({zkCdPsH82*@=nEE*>X zKrpcR@CBSIP)*%}+I{H)0RS9x$X~-p7q}O;-#PK1 TTF+(K=RI_rcbv{H0CNJh#g1(_M84-4M}D3Ee?S^%6f@A9BO w 6eaz*%cX|s{zG#5Xl`k>^aw}6-(&yjJ z0E{93dRx)~fC`#>{!^q2sDfK9KE)-qCT|34;|A?iNiLtb5^cJnh6sL5+bNog$(Qf> zRXZlekEkL^Luv8koTle2k^32ON_Y?}YX(T!BMu%BMeQ2{6t~JO+g8qZ@+bYR)zvVY zWeGv!Pm!+eUBWBnpmh)H?CAlO@aN_ZHwhf~t1BEVU-(g;q(x_>En6kwZTO5fe8<{= ztCj}*k~;Of +UtUOm^@8r1Q>N`rKj$q%7g2z@zlls6^gMkkuj~p-r((i zGC+mCqC v@-i1IaRB_2I`ZHQl^6We zvy3SyL((l# tSizU?yy_Fil~9{acVfea#=byW7KN` z22=9&@|sqx Tayo*8T3{K#+R0}d0CR>|Rs_4E_N5a&X4U@MRJoVE zNqsLd^hArZxzm%a8QpKcutNv)5NC+|`bgmIk*QwzeF{(*et$aJW0Efg>XF7-=ZaiB zF&`?LE5}c+@MYtovf~wLiT0a@*c u*0f+D%^%;Pp&r_wahX2F1d5g4+02LSd@sk|-jNkj2bwIRm^P&d64LC0(XZpUjD2 zs0tWh2XF-%ZFsQbHeg1~WFE0>z;F7Vufk!`o*pk9zD_lX<3`W3t+SvXHJ2hdL{I?U z78o8;lJ8!;OC-`#8r@%smgG}t8&(2OM5=0_ZRU#65Rgk+51t?r)OZh8L2Cz;a0mu> zpa5ak5SSE06L)|@Rl?m~_O_L HL;}HJ7Hm25 zj5c9E7`~8|dR02^gJHkI=-JR^f8IOv!HM_DU1LTJQhDub{k4UCkNsleRCA@2QS018 zePOh|bmpw;d>*i6E^f%`7@xf x)wOa1j}}H7YQ?;NUGM$sPfCkT2@e` z3( z)k!@FZlet7VJcpg|~H$6yCODOT@4A#zZ)^y$0Dik;1CxN?(8C9(0{RI5h+vYe0+ z{==sK01KNfo7YQG;W}MPE!+da1&4(nY8Ld-R>80p74b*xre*K?7b!hbg`XoAMY5ZZ zBT%a{<4j^-WEgUlG+8r@gpv^YGc5hjcO3YvRL$~WP1H8DF10sLPaUG?KsSA!_he=v zNM?<}85b)vV@=0%JRqJ6_wnv*f9g|$YDdm(KB?3ThU=3;UasQJf(mopsFY7M_PE1C zabjjd>j&5kd>y#-`(U$wy~-#ajgVae 8C8lP;6j77RWqC z-6)nl=Do9u+6^l8yqSa%7ZRbNXfOSF_6T%e<-wkB*7u?n9-ay4p%h9ZcYoK&z?~6= zH%Ri1zN(hEhCV;LxX46bHiM{u@6g6~#rMiW9h|!*m2&e__g7eu2aXx-{J3JM{nt@` z=KmZPe#Hg>L#Gc>Vj){2W2p~);y=TK^5m~(R@WKjkOd+QVv>Yc0~JVRqT&Uo-OK|4 z#88ly1a@5e`K3bB*>Qqpl<17uF%ybF7MYE1e!XOs_$zy)rT{|Y?d>F6JyZXeLd*|c zC#&cD%{Z!-mJrnZAn$3BzCoNJ{s})zw>yY)rqwy{>qFR>dev%mt2_ afrzRCn^Cj_HN_?|=1$-A$c@sY;GO;`C9xK=j>F!|iWDq0~9o$N& z>u*E)lRE-SDS Cu)0oV-p$jhVZ!_vaJ)hWK`CO-gC|K}wn@rt>_uTYm>t?YNt`oUG1X(0jGBMNMX| zzOD&Kr2D7@aA@y+J5O3#G93z6pt_yg)@LhLQ8(S2^>s6YCZZdJKiA0j!S1vMIzE1E zI*4x2TkUH;#=hi@Fl5h+WJM`e;;&$>|5zA$h)bt0{1A0IixYt^;{wc2Icz=q*qAIU zQWD`ncC1fn%-rUaR#jLGQ@z*{T_Xu;NH40yk8F}IkNM2TAc-o{wE5$BLcInLEVZH6 zDkumMCFOayHaSm~XyzhBFwV$XP{O zt$q080h#o ln!?ddbq9 zv$v$kMYAz77gxao?>>=;4A5)ZQ^u2BjU=HF_KJ}88|ONB%3)%hIyHW+HfyTW$49ui z8`yN2=#Wk5!t-()_cf~_-Zh9F1ty5*9me@SJ0=2PLh4vL*IR0gINUHDHjo|aj()FU zq}+|)Ks$+KuQ&Gn4@+l{VSddr5?0bG4H-@IRji{ZPsIz9Oy*sKiIQItr`DjPT~!wQMeAJ-#q za%iUDIs?v9%J#dFJD|3!*P>nVH+ciJknbJuelAv>p}VwBwRu;mg}qqZ)Wk+8v?KoX z%x!+IyLak#Sipws2BM)xtyd(8#C|HijkkBG7P`|gWJ-GfN8J!V)ZXXhBb3zrBa{FF z@d5v5CG(GX@(*k9{q^Zz1;ame&E+Kjit%^(=>I_g0BG=?{?MHGH)7Ji0e)*P|EUT5 zOPKDr75J`wxcyHj{;t6MC(JJa;Xefxe+dZxwgO+}kAcU3!Tc#F{2Sx9viskq7k^_k z`0B#^iSbV%#@_(H&hkrS?@xiQzn-ND?oWV!6lDJ;x%Vf`FEy_}rLKO7$NaVe--M4m z@n2{8-`ZY(BK?w~`O~3a(pbN(z&G||J^x=wzjDg|;?yswKecXtY4H5E0$)e;zeD|+ zpZKlF^LLm2lq31;;Ycz59nQaWa{i9=C%fn``I6sO;5+u=;9n2-%cFn3@{uC{9qUiV zf?s^3zpcPm &+4*e+zA zex83VcnR{vFtVzO?=w`Okk`zcSt5R^aRH_8*b|Yu+m-3Hs4M0{}q(_;~;LYQ*c; GcmEgP3@Jze literal 0 HcmV?d00001 diff --git a/tools/rst-optimizer/src/config.ts b/tools/rst-optimizer/src/config.ts new file mode 100644 index 0000000000..a4d79a030e --- /dev/null +++ b/tools/rst-optimizer/src/config.ts @@ -0,0 +1,65 @@ +import * as vscode from 'vscode'; +import { OptimizationConfig } from './types'; + +export class ConfigManager { + private static readonly CONFIG_SECTION = 'rstOptimizer'; + + static getConfig(): OptimizationConfig { + const config = vscode.workspace.getConfiguration(this.CONFIG_SECTION); + + // 优先从环境变量读取 API Key + const apiKey = process.env.RST_OPTIMIZER_API_KEY || config.get ('api.apiKey', ''); + + return { + provider: config.get ('api.provider', 'openai-compatible'), + baseUrl: config.get ('api.baseUrl', 'https://api.openai.com/v1'), + model: config.get ('api.model', 'gpt-4o-mini'), + apiKey, + userPrompt: config.get ('prompt.userPrompt',"你是一名 **RST 技术文档审校与修复专家**。\n你的任务是:在**不改变技术语义**的前提下,**严格检查并修复**给定 RST 文档在语法、结构、格式与中文排版上的问题,并**保持与 API 定义及文件名的一致性**。\n如无明确要求,**不要引入新的内容段落或编造默认值/异常**;仅在缺漏“必须存在且可从上下文直接确定”的字段时做最小增补。\n\n## 输入\n\n* 文件名:${fileName}\n* 原始 RST 文本(保持原样)\n\n## 总体原则\n\n1. **最小侵入修复**:仅修复错误或不规范之处;保留原有信息结构与术语。\n2. **禁止误改代码/公式**:`code-block`、`literalinclude`、`math` 等指令体及反引号行内代码的技术内容不得改写(仅可做空格与围栏修复)。\n3. **一致性优先**:文件名、章节标题、API 名称必须一致(见规则 2)。\n4. **中文排版**:面向中文读者的正文需优化中文标点与用语,但不得改变技术意义。\n5. **不可臆测**:若无默认值/异常/类型信息,不得凭空添加。无法确定时保持空或原状,并在总结里标注“需要人工确认”。\n\n## 必查必修规则(逐条执行)\n\n### 1)通用 RST 语法与特殊标记\n\n* 检查并修复常见指令语法:`.. note::`、`.. warning::`、`.. seealso::`、`.. include::`、`.. math::` 等的缩进与空行:\n * 指令与其内容之间需空一行(除确有嵌套要求的场景外)。\n * 指令体内容相对指令行统一缩进(建议 3–4 空格),全文保持一致。\n* 行内特殊标记规则:\n * `*斜体*`、`**加粗**`、``行内代码``:标记内部不得出现空格(确需空格时改为转义或拆分)。\n * 标记与上下文:标记前后各保留 1 个空格(句首或紧邻标点处除外)。\n* 修复多余反引号、未闭合标记、错误嵌套。\n\n### 2)文件名 / 章节标题 / API 定义名一致性\n\n* 要求:文件名、文档首个最高层级标题、正文首个 API 定义名应一致。\n* 例外:若文件名匹配 `mindspore.xxx.func_yyy.rst`,则章节标题与 API 定义名统一为 `mindspore.xxx.yyy`(去掉 `func_`)。\n* 实施:\n * 若三者不一致,以 API 定义名为准统一章节标题;遇到上述例外时按例外规则处理。\n * 若文件名与 API 命名空间明显冲突(如包路径不同),不改文件名,仅统一标题与文内 API 显示。\n\n### 3)“参数/关键字参数”模块格式(**包含关键字专用参数的严格规则**)\n\n* 目标格式(逐项以无序列表 `-` 起):\n\n```\n参数:\n- **参数名** (数据类型[, 可选]) – 参数说明。默认值:`None`,表示……\n 关键字参数:\n- **参数名** (数据类型[, 可选]) – 参数说明。默认值:`None`,表示……\n```\n\n* 严格对齐 Python 函数签名的五类参数并分流到正确模块:\n\n1) **仅位置参数(positional-only)**:位于 `/` 左侧(若签名包含 `/`)。归入“参数”模块,**不要展示 `/` 本身**。\n2) **位置或关键字参数(positional-or-keyword)**:常见的 `name` 或 `name=...`。归入“参数”模块。\n3) **可变位置参数**:`*args`。归入“参数”模块,名称保留星号前缀 `*`,类型与说明按项目规范书写。\n4) **关键字专用参数(keyword-only)**:当签名中出现 **裸 `*` 分隔符** 或 `*args` 之后的形参(直到 `**kwargs` 之前)。**这些形参必须归入“关键字参数”模块**。\n - 裸 `*` 仅是分隔符,**不出现在文档中**;其右侧的参数(如 `dtype=None`)统一移至“关键字参数”。\n5) **可变关键字参数**:`**kwargs`。根据项目约定决定是否在“关键字参数”模块列出;若列出需保留 `**` 前缀并简要说明用途,避免臆测具体键。\n\n* “参数/关键字参数”的**名称与顺序必须与函数定义完全一致**(先左后右、从签名原序列化得到)。\n* “可选/默认值”标注规则:\n* 形参**有默认值**(含 `=None`)或注解为可选类型时:在类型后补 `, 可选`,且在说明末尾追加“默认值:`` ``,……”。\n* 形参**无默认值**:不写“可选”,不写默认值句。\n* **数据类型**需与定义一致;若原文缺失且上下文也无法确定,不要臆测,类型可暂缺省或保持原状。\n* 术语与标点:\n* 中文破折号统一使用 `–`(en dash),全文保持一致。\n* 默认值文字中的字面量使用行内代码围栏(如 ``None``、``True``、``-1``)。\n* **迁移示例(与你给的 case 对齐)**:签名 `(..., dim=None, *, dtype=None)` →\n `dim` 仍在“参数”;`dtype` 作为 **关键字专用参数** 移至“关键字参数”。\n\n### 4)“异常”模块格式\n\n* 目标格式:\n\n```\n异常:\n- **ErrorType** - 异常描述。\n```\n\n* 同类异常归并在一起,子类在前、父类在后;不得杜撰异常,无法确认时保留原状并在总结中标注需确认。\n\n### 5)“输入/输出”模块格式\n\n* 目标格式:\n\n```\n输入:\n- **输入名** (数据类型) – 描述。\n 输出:\n- **输出名** (数据类型) – 描述。\n```\n\n* 无序列表 `-`,加粗名称 + 圆括号数据类型 + `–` 描述。\n\n### 6)换行与缩进\n\n* 普通段落换行不缩进。\n* 有序/无序列表换行统一 2 个空格缩进,与上一行正文起始位置对齐。\n* 指令体(note/warning/seealso/include/math 等)内层统一缩进;子块(如代码)再按 RST 规范缩进一级。\n\n### 7)模块间空行\n\n* 各模块之间至少 1 个空行;指令与其前后段落之间保留空行,避免黏连。\n\n### 8)中文文本与排版\n\n* 修复错别字、冗余空格、英文标点混用(中文语句中使用中文标点:`,` `。` `:` `;` `( )`)。\n* 并列关系用顿号 `、` 分隔。\n* API 名、类名、参数名、代码标识符保留原文并用行内代码``包裹。\n* 句子更通顺简洁,但不得改变技术含义。\n\n## 额外一致性检查\n\n* 标题层级符号(`= - ~ ^ \" ' *` 等)长度需与标题文本长度一致;同级标题符号统一。\n* `:param:/:type:/:return:/:rtype:` 若与目标“列表式参数节”并存,应二选一统一为项目约定风格(通常统一为“参数/关键字参数”块)。\n* 交叉引用(`:class:`, `:func:`, `:mod:` 等)语法修复:角色名、目标、反引号与空格。\n* `include` 路径前后空格与相对路径的一致性。\n\n## 输出要求\n\n* 输出修复后的完整 RST 正文(不加多余说明或包裹)。\n* 如需 diff 模式,由外层系统控制;本指令默认仅产出修复后全文。\n\n## 审核自查清单(模型内部执行,无需输出)\n\n* [ ] 指令语法/缩进/空行正确\n* [ ] 行内标记无空格、边界空格正确\n* [ ] 文件名/标题/API 名一致(含 `func_` 例外)\n* [ ] 参数/关键字参数:顺序与分流严格按签名(含 `/`、裸 `*`、`*args`、`**kwargs`)\n* [ ] “可选/默认值”标注与默认值文字格式正确\n* [ ] 异常:格式统一、同类聚合、无臆测\n* [ ] 输入/输出:列表格式统一\n* [ ] 列表缩进 2 空格,段落不缩进\n* [ ] 模块间有空行(例如返回是一个模块,异常是一个模块,他们之间有空行,但是异常下面的具体异常和异常大标题之间无空行)\n* [ ] 中文标点与顿号、错别字修复\n* [ ] 代码/公式内容未被改写(仅围栏/空格修复)"), + maxTokens: config.get ('generation.maxTokens', 4096), + temperature: config.get ('generation.temperature', 0.3), + neverUploadIfWorkspaceTrusted: config.get ('safety.neverUploadIfWorkspaceTrusted', false), + rewrapWidth: config.get ('format.rewrapWidth', 0) + }; + } + + static validateConfig(config: OptimizationConfig): string[] { + const errors: string[] = []; + + if (!config.baseUrl) { + errors.push('API 基础 URL 不能为空'); + } + + if (!config.model) { + errors.push('模型名称不能为空'); + } + + if (!config.apiKey) { + errors.push('API 密钥不能为空,请在设置中配置或设置环境变量 RST_OPTIMIZER_API_KEY'); + } + + if (config.maxTokens <= 0) { + errors.push('最大 token 数量必须大于 0'); + } + + if (config.temperature < 0 || config.temperature > 2) { + errors.push('温度值必须在 0-2 之间'); + } + + return errors; + } + + static checkWorkspaceSafety(): boolean { + const workspaceTrust = vscode.workspace.isTrusted; + const config = this.getConfig(); + + if (config.neverUploadIfWorkspaceTrusted && workspaceTrust) { + vscode.window.showWarningMessage( + '安全设置阻止了在受信任工作区中上传内容到外部 API。请在设置中关闭 "neverUploadIfWorkspaceTrusted" 选项。' + ); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/tools/rst-optimizer/src/diff/diffActionCodeLensProvider.ts b/tools/rst-optimizer/src/diff/diffActionCodeLensProvider.ts new file mode 100644 index 0000000000..0021d9b60e --- /dev/null +++ b/tools/rst-optimizer/src/diff/diffActionCodeLensProvider.ts @@ -0,0 +1,37 @@ +import * as vscode from 'vscode'; +import { VirtualDocProvider } from './virtualDocProvider'; + + +export class DiffActionCodeLensProvider implements vscode.CodeLensProvider { + private onDidChangeCodeLensesEmitter = new vscode.EventEmitter (); + public readonly onDidChangeCodeLenses: vscode.Event = this.onDidChangeCodeLensesEmitter.event; + + provideCodeLenses(document: vscode.TextDocument): vscode.ProviderResult { + if (document.uri.scheme !== VirtualDocProvider.getScheme()) { + return []; + } + if (!document.uri.path.startsWith('/optimized/')) { + return []; + } + + const lastLine = Math.max(0, document.lineCount - 1); + const range = new vscode.Range(lastLine, 0, lastLine, 0); + + const applyLens = new vscode.CodeLens(range, { + title: '$(check) 应用优化', + command: 'rstOptimizer.applyResult' + }); + + const discardLens = new vscode.CodeLens(range, { + title: '$(x) 放弃优化', + command: 'rstOptimizer.discardResult' + }); + + return [applyLens, discardLens]; + } + + refresh(): void { + this.onDidChangeCodeLensesEmitter.fire(); + } +} + diff --git a/tools/rst-optimizer/src/diff/virtualDocProvider.ts b/tools/rst-optimizer/src/diff/virtualDocProvider.ts new file mode 100644 index 0000000000..8fe3859564 --- /dev/null +++ b/tools/rst-optimizer/src/diff/virtualDocProvider.ts @@ -0,0 +1,42 @@ +import * as vscode from 'vscode'; + +export class VirtualDocProvider implements vscode.TextDocumentContentProvider { + private static readonly scheme = 'rstopt'; + private contentMap = new Map (); + private _onDidChange = new vscode.EventEmitter (); + + readonly onDidChange = this._onDidChange.event; + + static createUri(type: 'original' | 'optimized', filePath: string): vscode.Uri { + const timestamp = Date.now(); + return vscode.Uri.parse(`${VirtualDocProvider.scheme}:${type}/${encodeURIComponent(filePath)}?t=${timestamp}`); + } + + provideTextDocumentContent(uri: vscode.Uri): string | undefined { + const key = this.getKeyFromUri(uri); + return this.contentMap.get(key); + } + + set(uri: vscode.Uri, content: string): void { + const key = this.getKeyFromUri(uri); + this.contentMap.set(key, content); + this._onDidChange.fire(uri); + } + + clear(uri: vscode.Uri): void { + const key = this.getKeyFromUri(uri); + this.contentMap.delete(key); + } + + clearAll(): void { + this.contentMap.clear(); + } + + private getKeyFromUri(uri: vscode.Uri): string { + return `${uri.path}${uri.query}`; + } + + static getScheme(): string { + return VirtualDocProvider.scheme; + } +} \ No newline at end of file diff --git a/tools/rst-optimizer/src/extension.ts b/tools/rst-optimizer/src/extension.ts new file mode 100644 index 0000000000..1e71f29b7c --- /dev/null +++ b/tools/rst-optimizer/src/extension.ts @@ -0,0 +1,452 @@ +import * as vscode from 'vscode'; +import { ConfigManager } from './config'; +import { LLMClient } from './llm/client'; +import { VirtualDocProvider } from './diff/virtualDocProvider'; +import { DiffActionCodeLensProvider } from './diff/diffActionCodeLensProvider'; +import { BatchResultsViewProvider } from './views/batchResultsView'; +import type { BatchResultItem } from './views/batchResultsHtml'; +import { FileUtils } from './utils/file'; +import { TextWrapper } from './utils/wrap'; +import { DiffContext } from './types'; +import { HistoryStore } from './history'; + +let outputChannel: vscode.OutputChannel; +let llmClient: LLMClient; +let virtualDocProvider: VirtualDocProvider; +let statusBarItem: vscode.StatusBarItem; +let currentDiffContext: DiffContext | undefined; +let batchResultsViewProvider: BatchResultsViewProvider; + +export function activate(context: vscode.ExtensionContext) { + outputChannel = vscode.window.createOutputChannel('RST Optimizer'); + context.subscriptions.push(outputChannel); + + llmClient = new LLMClient(outputChannel); + + HistoryStore.init(context); + + virtualDocProvider = new VirtualDocProvider(); + context.subscriptions.push( + vscode.workspace.registerTextDocumentContentProvider( + VirtualDocProvider.getScheme(), + virtualDocProvider + ) + ); + + const codeLensProvider = new DiffActionCodeLensProvider(); + context.subscriptions.push( + vscode.languages.registerCodeLensProvider( + { scheme: VirtualDocProvider.getScheme() }, + codeLensProvider + ) + ); + + batchResultsViewProvider = new BatchResultsViewProvider({ + onApplyAll: async (results) => applyAllBatchResults(results), + onApplySelected: async (results, selected) => applySelectedBatchResults(results, selected), + onViewDiffById: async (id) => { + const r = HistoryStore.getById(id); + if (r) { + await showDiffView(r.filePath, r.originalText, r.optimizedText); + } + }, + onDiscard: async () => {/* no-op, close action handled by view */} + }); + context.subscriptions.push( + vscode.window.registerWebviewViewProvider(BatchResultsViewProvider.ViewId, batchResultsViewProvider) + ); + // 命令:确保可以显式聚焦视图 + context.subscriptions.push( + vscode.commands.registerCommand('rstOptimizer.revealBatchResultsView', () => batchResultsViewProvider.reveal()) + ); + + // 创建状态栏项 + statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 100); + statusBarItem.text = '$(edit) RST Optimizer'; + statusBarItem.tooltip = '点击打开 RST Optimizer 命令'; + statusBarItem.command = 'rstOptimizer.showQuickPick'; + statusBarItem.show(); + context.subscriptions.push(statusBarItem); + + // 注册命令 + const commands = [ + vscode.commands.registerCommand('rstOptimizer.optimizeCurrent', optimizeCurrentFile), + vscode.commands.registerCommand('rstOptimizer.optimizePickFile', optimizePickedFile), + vscode.commands.registerCommand('rstOptimizer.applyResult', applyOptimizedResult), + vscode.commands.registerCommand('rstOptimizer.discardResult', discardOptimizedResult), + vscode.commands.registerCommand('rstOptimizer.openSettings', openSettings), + vscode.commands.registerCommand('rstOptimizer.showQuickPick', showQuickPick) + ]; + + context.subscriptions.push(...commands); + + outputChannel.appendLine('RST Optimizer 扩展已激活'); +} + +export function deactivate() { + if (virtualDocProvider) { + virtualDocProvider.clearAll(); + } + outputChannel?.appendLine('RST Optimizer 扩展已停用'); +} + +async function optimizeCurrentFile() { + const filePath = FileUtils.getCurrentRstFile(); + if (!filePath) { + vscode.window.showWarningMessage('当前编辑器不是 RST 文件,请打开一个 .rst 文件'); + return; + } + + await optimizeFile(filePath); +} + +async function optimizePickedFile() { + const filePaths = await FileUtils.pickRstFiles(); + if (filePaths.length === 0) { + return; // 用户取消了选择 + } + + // 打开选中的文件 + for (const filePath of filePaths) { + const document = await vscode.workspace.openTextDocument(filePath); + await vscode.window.showTextDocument(document, { preview: false }); + } + + if (filePaths.length === 1) { + // 单文件优化 + await optimizeFile(filePaths[0]); + } else { + // 批量优化 + await optimizeMultipleFiles(filePaths); + } +} + +async function optimizeMultipleFiles(filePaths: string[]) { + try { + // 检查工作区安全设置 + if (!ConfigManager.checkWorkspaceSafety()) { + return; + } + + // 获取配置 + const config = ConfigManager.getConfig(); + const configErrors = ConfigManager.validateConfig(config); + if (configErrors.length > 0) { + vscode.window.showErrorMessage(`配置错误:${configErrors.join(', ')}`); + return; + } + + const results: { filePath: string; originalText: string; optimizedText: string }[] = []; + + // 显示总体进度 + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: `正在批量优化 ${filePaths.length} 个 RST 文件...`, + cancellable: true + }, + async (progress, token) => { + for (let i = 0; i < filePaths.length; i++) { + const filePath = filePaths[i]; + const fileName = FileUtils.getFileName(filePath); + + if (token.isCancellationRequested) { + break; + } + + progress.report({ + message: `正在优化 ${fileName} (${i + 1}/${filePaths.length})`, + increment: (100 / filePaths.length) + }); + + try { + const originalText = await FileUtils.readFile(filePath); + if (!originalText.trim()) { + outputChannel.appendLine(`跳过空文件: ${filePath}`); + continue; + } + + const dotIdx = fileName.lastIndexOf('.'); + const fileBaseName = dotIdx > 0 ? fileName.slice(0, dotIdx) : fileName; + const fileExt = dotIdx > -1 ? fileName.slice(dotIdx + 1) : ''; + const optimizedText = await llmClient.optimizeRst( + { + text: originalText, + userPrompt: config.userPrompt, + config, + variables: { + filePath, + fileName, + relativePath: FileUtils.getRelativePath(filePath), + fileBaseName, + fileExt + } + }, + token + ); + + const finalOptimizedText = config.rewrapWidth > 0 + ? TextWrapper.wrapText(optimizedText, config.rewrapWidth) + : optimizedText; + + results.push({ + filePath, + originalText, + optimizedText: finalOptimizedText + }); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + outputChannel.appendLine(`优化失败 ${filePath}: ${errorMessage}`); + vscode.window.showWarningMessage(`优化 ${fileName} 失败: ${errorMessage}`); + } + } + } + ); + + if (results.length > 0) { + await showBatchOptimizationResults(results); + } else { + vscode.window.showWarningMessage('没有成功优化任何文件'); + } + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + vscode.window.showErrorMessage(`批量优化失败: ${errorMessage}`); + outputChannel.appendLine(`批量优化失败: ${errorMessage}`); + } +} + +async function optimizeFile(filePath: string) { + try { + // 检查工作区安全设置 + if (!ConfigManager.checkWorkspaceSafety()) { + return; + } + + // 获取配置 + const config = ConfigManager.getConfig(); + const configErrors = ConfigManager.validateConfig(config); + if (configErrors.length > 0) { + vscode.window.showErrorMessage(`配置错误:${configErrors.join(', ')}`); + return; + } + + // 读取文件内容 + const originalText = await FileUtils.readFile(filePath); + if (!originalText.trim()) { + vscode.window.showWarningMessage('文件内容为空'); + return; + } + + // 显示进度并执行优化 + const optimizedText = await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: `正在使用 ${config.provider}/${config.model} 优化 RST 文档...`, + cancellable: true + }, + async (progress, token) => { + const fileName = FileUtils.getFileName(filePath); + const relativePath = FileUtils.getRelativePath(filePath); + const dotIdx = fileName.lastIndexOf('.'); + const fileBaseName = dotIdx > 0 ? fileName.slice(0, dotIdx) : fileName; + const fileExt = dotIdx > -1 ? fileName.slice(dotIdx + 1) : ''; + return await llmClient.optimizeRst( + { + text: originalText, + userPrompt: config.userPrompt, + config, + variables: { filePath, fileName, relativePath, fileBaseName, fileExt } + }, + token + ); + } + ); + + // 应用文本包装(如果配置了) + const finalOptimizedText = config.rewrapWidth > 0 + ? TextWrapper.wrapText(optimizedText, config.rewrapWidth) + : optimizedText; + + // 创建 diff 视图 + await showDiffView(filePath, originalText, finalOptimizedText); + + // 记录到历史并刷新侧边栏 + await batchResultsViewProvider.showResults([{ filePath, originalText, optimizedText: finalOptimizedText }]); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + vscode.window.showErrorMessage(`优化失败: ${errorMessage}`); + outputChannel.appendLine(`优化失败: ${errorMessage}`); + } +} + +async function showDiffView(filePath: string, originalText: string, optimizedText: string) { + // 创建虚拟 URI + const originalUri = VirtualDocProvider.createUri('original', filePath); + const optimizedUri = VirtualDocProvider.createUri('optimized', filePath); + + // 设置虚拟文档内容 + virtualDocProvider.set(originalUri, originalText); + virtualDocProvider.set(optimizedUri, optimizedText); + + // 保存当前 diff 上下文 + currentDiffContext = { + originalUri, + optimizedUri, + filePath, + optimizedContent: optimizedText + }; + + // 打开 diff 视图 + const fileName = FileUtils.getFileName(filePath); + const title = `RST Diff: ${fileName}`; + + await vscode.commands.executeCommand( + 'vscode.diff', + originalUri, + optimizedUri, + title, + { preview: false } + ); + + // 设置上下文,确保菜单按钮在 diff 标题栏显示 + await vscode.commands.executeCommand('setContext', 'rstOptimizer.hasActiveDiff', true); +} + +async function showBatchOptimizationResults(results: { filePath: string; originalText: string; optimizedText: string }[]) { + await batchResultsViewProvider.showResults(results); +} + + +async function applyOptimizedResult() { + if (!currentDiffContext) { + vscode.window.showWarningMessage('没有可应用的优化结果'); + return; + } + + try { + await FileUtils.writeFile(currentDiffContext.filePath, currentDiffContext.optimizedContent); + + const fileName = FileUtils.getFileName(currentDiffContext.filePath); + vscode.window.showInformationMessage(`已成功应用优化结果到 ${fileName}`); + + outputChannel.appendLine(`[${new Date().toISOString()}] 已应用优化结果: ${currentDiffContext.filePath}`); + + // 清理 + await discardChanges(); + + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + vscode.window.showErrorMessage(`应用更改失败: ${errorMessage}`); + } +} + +async function discardOptimizedResult() { + if (!currentDiffContext) { + vscode.window.showWarningMessage('没有可放弃的优化结果'); + return; + } + + const fileName = FileUtils.getFileName(currentDiffContext.filePath); + vscode.window.showInformationMessage(`已放弃对 ${fileName} 的优化结果`); + + outputChannel.appendLine(`[${new Date().toISOString()}] 已放弃优化结果: ${currentDiffContext.filePath}`); + + // 清理 + await discardChanges(); +} + +async function discardChanges() { + if (currentDiffContext) { + // 清理虚拟文档 + virtualDocProvider.clear(currentDiffContext.originalUri); + virtualDocProvider.clear(currentDiffContext.optimizedUri); + currentDiffContext = undefined; + } + // 清理上下文 + await vscode.commands.executeCommand('setContext', 'rstOptimizer.hasActiveDiff', false); +} + +async function applyAllBatchResults(results: { filePath: string; originalText: string; optimizedText: string }[]) { + let successCount = 0; + let failCount = 0; + + for (const result of results) { + try { + await FileUtils.writeFile(result.filePath, result.optimizedText); + successCount++; + outputChannel.appendLine(`[${new Date().toISOString()}] 已应用优化结果: ${result.filePath}`); + } catch (error) { + failCount++; + const errorMessage = error instanceof Error ? error.message : String(error); + outputChannel.appendLine(`[${new Date().toISOString()}] 应用失败: ${result.filePath} - ${errorMessage}`); + } + } + + vscode.window.showInformationMessage( + `批量应用完成!成功: ${successCount} 个,失败: ${failCount} 个` + ); +} + +async function applySelectedBatchResults( + results: { filePath: string; originalText: string; optimizedText: string }[], + selectedFiles: string[] +) { + let successCount = 0; + let failCount = 0; + + for (const filePath of selectedFiles) { + const result = results.find(r => r.filePath === filePath); + if (!result) continue; + + try { + await FileUtils.writeFile(result.filePath, result.optimizedText); + successCount++; + outputChannel.appendLine(`[${new Date().toISOString()}] 已应用优化结果: ${result.filePath}`); + } catch (error) { + failCount++; + const errorMessage = error instanceof Error ? error.message : String(error); + outputChannel.appendLine(`[${new Date().toISOString()}] 应用失败: ${result.filePath} - ${errorMessage}`); + } + } + + vscode.window.showInformationMessage( + `选择性应用完成!成功: ${successCount} 个,失败: ${failCount} 个` + ); +} + +// batch 结果的 HTML 已移动到 src/views/batchResultsHtml.ts + +async function openSettings() { + await vscode.commands.executeCommand('workbench.action.openSettings', 'rstOptimizer'); +} + +async function showQuickPick() { + const items = [ + { + label: '$(file-text) 优化当前 RST 文件', + description: '优化当前活动编辑器中的 RST 文件', + command: 'rstOptimizer.optimizeCurrent' + }, + { + label: '$(folder-opened) 选择 RST 文件优化', + description: '通过文件选择器选择要优化的 RST 文件', + command: 'rstOptimizer.optimizePickFile' + }, + { + label: '$(settings-gear) 打开设置', + description: '配置 RST Optimizer 设置', + command: 'rstOptimizer.openSettings' + } + ]; + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: '选择要执行的操作' + }); + + if (selected) { + await vscode.commands.executeCommand(selected.command); + } +} diff --git a/tools/rst-optimizer/src/history.ts b/tools/rst-optimizer/src/history.ts new file mode 100644 index 0000000000..5b007aae98 --- /dev/null +++ b/tools/rst-optimizer/src/history.ts @@ -0,0 +1,58 @@ +import * as vscode from 'vscode'; +import type { BatchResultItem } from './views/batchResultsHtml'; + + +export class HistoryStore { + private static context: vscode.ExtensionContext | undefined; + private static readonly KEY = 'rstOptimizer.history'; + + static init(context: vscode.ExtensionContext) { + this.context = context; + } + + private static ensureReady() { + if (!this.context) { + throw new Error('HistoryStore not initialized'); + } + } + + static getAll(): BatchResultItem[] { + this.ensureReady(); + const items = this.context!.globalState.get (this.KEY, []); + return Array.isArray(items) ? items.slice().sort((a, b) => (b.timestamp ?? 0) - (a.timestamp ?? 0)) : []; + } + + static addMany(items: Omit []): BatchResultItem[] { + this.ensureReady(); + const existing = this.getAll(); + const now = Date.now(); + const withIds: BatchResultItem[] = items.map((it, idx) => ({ + ...it, + id: `${now}-${idx}-${Math.random().toString(36).slice(2, 8)}`, + timestamp: now + idx, + })); + const next = [...withIds, ...existing]; + this.context!.globalState.update(this.KEY, next); + return withIds; + } + + static addOne(item: Omit ): BatchResultItem { + return this.addMany([item])[0]; + } + + static removeById(id: string): void { + this.ensureReady(); + const next = this.getAll().filter(it => it.id !== id); + this.context!.globalState.update(this.KEY, next); + } + + static getById(id: string): BatchResultItem | undefined { + return this.getAll().find(it => it.id === id); + } + + static clearAll(): void { + this.ensureReady(); + this.context!.globalState.update(this.KEY, []); + } +} + diff --git a/tools/rst-optimizer/src/llm/client.ts b/tools/rst-optimizer/src/llm/client.ts new file mode 100644 index 0000000000..7985b498aa --- /dev/null +++ b/tools/rst-optimizer/src/llm/client.ts @@ -0,0 +1,117 @@ +import * as vscode from 'vscode'; +import { OptimizationRequest, LLMResponse } from '../types'; + +const DEFAULT_SYSTEM_PROMPT = `你是 reStructuredText(RST)与 Sphinx 文档优化专家。目标:在不破坏语义与构建的前提下,让文档更清晰专业。 +严格遵循: +1) 保留并尊重所有 Sphinx 角色/指令/域(如 :ref:、:class:、:func:、.. code-block::、.. note::、.. figure:: 等),不要更改其语法结构与缩进。 +2) 绝不删除或更改链接目标、交叉引用锚点(如 .. _anchor:)。 +3) 保留代码块、控制台示例与行内字面值(\`\`literal\`\`)原样;除非是修复明显拼写错误。 +4) 标题层级与下划线风格统一(如 # * = - ^ ~ 等),但不得改变层级关系。 +5) 修复语法/拼写/术语一致性,使段落更简洁、技术准确;尽量减少被动语态。 +6) 表格、列表、缩进必须合法;指令体的缩进四空格对齐。 +7) 输出**完整优化后的整篇 RST**,不要输出 diff 或注释。`; + +export class LLMClient { + private outputChannel: vscode.OutputChannel; + + constructor(outputChannel: vscode.OutputChannel) { + this.outputChannel = outputChannel; + } + + async optimizeRst( + request: OptimizationRequest, + cancellationToken?: vscode.CancellationToken + ): Promise { + const startTime = Date.now(); + const { text, userPrompt, config } = request; + + this.outputChannel.appendLine(`[${new Date().toISOString()}] 开始优化 RST 文档`); + this.outputChannel.appendLine(`提供商: ${config.provider}`); + this.outputChannel.appendLine(`模型: ${config.model}`); + this.outputChannel.appendLine(`文档长度: ${text.length} 字符`); + this.outputChannel.appendLine(`估算 token: ${Math.ceil(text.length / 4)}`); + + try { + const userContent = this.composeUserContent(text, userPrompt, request.variables); + + const requestBody = { + model: config.model, + temperature: config.temperature, + max_tokens: config.maxTokens, + messages: [ + { role: "system", content: DEFAULT_SYSTEM_PROMPT }, + { role: "user", content: userContent } + ] + }; + + const controller = new AbortController(); + + // 处理取消令牌 + if (cancellationToken) { + cancellationToken.onCancellationRequested(() => { + controller.abort(); + this.outputChannel.appendLine(`[${new Date().toISOString()}] 请求被用户取消`); + }); + } + + const response = await fetch(`${config.baseUrl}/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${config.apiKey}` + }, + body: JSON.stringify(requestBody), + signal: controller.signal + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP ${response.status}: ${errorText}`); + } + + const result = await response.json() as LLMResponse; + + if (!result.choices || result.choices.length === 0) { + throw new Error('API 返回了空的选择列表'); + } + + const optimizedText = result.choices[0].message.content; + const endTime = Date.now(); + const duration = endTime - startTime; + + this.outputChannel.appendLine(`[${new Date().toISOString()}] 优化完成`); + this.outputChannel.appendLine(`耗时: ${duration}ms`); + + if (result.usage) { + this.outputChannel.appendLine(`Token 使用: ${result.usage.prompt_tokens} + ${result.usage.completion_tokens} = ${result.usage.total_tokens}`); + } + + return optimizedText; + + } catch (error) { + const endTime = Date.now(); + const duration = endTime - startTime; + + this.outputChannel.appendLine(`[${new Date().toISOString()}] 优化失败 (${duration}ms)`); + + if (error instanceof Error) { + this.outputChannel.appendLine(`错误: ${error.message}`); + if (error.stack) { + this.outputChannel.appendLine(`堆栈: ${error.stack}`); + } + } else { + this.outputChannel.appendLine(`未知错误: ${String(error)}`); + } + + throw error; + } + } + + private composeUserContent(text: string, userPrompt: string, variables?: Record ): string { + const replaced = variables + ? userPrompt.replace(/\$\{(\w+)\}/g, (_m, k) => (variables[k] ?? ` +${'${'}${k}}`)) + : userPrompt; + return `${replaced}\n\n以下是需要优化的 RST 文档内容:\n\n${text}`; + } +} diff --git a/tools/rst-optimizer/src/test/extension.test.ts b/tools/rst-optimizer/src/test/extension.test.ts new file mode 100644 index 0000000000..4ca0ab4198 --- /dev/null +++ b/tools/rst-optimizer/src/test/extension.test.ts @@ -0,0 +1,15 @@ +import * as assert from 'assert'; + +// You can import and use all API from the 'vscode' module +// as well as import your extension to test it +import * as vscode from 'vscode'; +// import * as myExtension from '../../extension'; + +suite('Extension Test Suite', () => { + vscode.window.showInformationMessage('Start all tests.'); + + test('Sample test', () => { + assert.strictEqual(-1, [1, 2, 3].indexOf(5)); + assert.strictEqual(-1, [1, 2, 3].indexOf(0)); + }); +}); diff --git a/tools/rst-optimizer/src/types.ts b/tools/rst-optimizer/src/types.ts new file mode 100644 index 0000000000..8308b4a577 --- /dev/null +++ b/tools/rst-optimizer/src/types.ts @@ -0,0 +1,46 @@ +import * as vscode from 'vscode'; + +export interface OptimizationConfig { + provider: string; + baseUrl: string; + model: string; + apiKey: string; + userPrompt: string; + maxTokens: number; + temperature: number; + neverUploadIfWorkspaceTrusted: boolean; + rewrapWidth: number; +} + +export interface OptimizationRequest { + text: string; + userPrompt: string; + config: OptimizationConfig; + variables?: Record ; // e.g. { fileName, filePath, relativePath, fileBaseName, fileExt } +} + +export interface OptimizationResult { + optimizedText: string; + originalText: string; + filePath: string; +} + +export interface LLMResponse { + choices: Array<{ + message: { + content: string; + }; + }>; + usage?: { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + }; +} + +export interface DiffContext { + originalUri: vscode.Uri; + optimizedUri: vscode.Uri; + filePath: string; + optimizedContent: string; +} diff --git a/tools/rst-optimizer/src/utils/file.ts b/tools/rst-optimizer/src/utils/file.ts new file mode 100644 index 0000000000..b487da6216 --- /dev/null +++ b/tools/rst-optimizer/src/utils/file.ts @@ -0,0 +1,117 @@ +import * as vscode from 'vscode'; +import * as fs from 'fs'; +import * as path from 'path'; + +export class FileUtils { + /** + * 读取文件内容 + */ + static async readFile(filePath: string): Promise { + try { + const content = await fs.promises.readFile(filePath, 'utf8'); + return content; + } catch (error) { + throw new Error(`读取文件失败: ${error instanceof Error ? error.message : String(error)}`); + } + } + + /** + * 写入文件内容 + */ + static async writeFile(filePath: string, content: string): Promise { + try { + await fs.promises.writeFile(filePath, content, 'utf8'); + } catch (error) { + throw new Error(`写入文件失败: ${error instanceof Error ? error.message : String(error)}`); + } + } + + /** + * 获取当前活动编辑器的文件路径 + */ + static getCurrentRstFile(): string | undefined { + const activeEditor = vscode.window.activeTextEditor; + if (!activeEditor) { + return undefined; + } + + const document = activeEditor.document; + if (document.languageId !== 'restructuredtext' && !document.fileName.endsWith('.rst')) { + return undefined; + } + + return document.fileName; + } + + /** + * 显示文件选择器,只选择 .rst 文件 + */ + static async pickRstFile(): Promise { + const options: vscode.OpenDialogOptions = { + canSelectMany: false, + openLabel: '选择 RST 文件', + filters: { + 'reStructuredText 文件': ['rst'], + '所有文件': ['*'] + } + }; + + const fileUri = await vscode.window.showOpenDialog(options); + if (fileUri && fileUri[0]) { + return fileUri[0].fsPath; + } + + return undefined; + } + + /** + * 显示文件选择器,支持多选 .rst 文件 + */ + static async pickRstFiles(): Promise { + const options: vscode.OpenDialogOptions = { + canSelectMany: true, + openLabel: '选择 RST 文件(支持多选)', + filters: { + 'reStructuredText 文件': ['rst'], + '所有文件': ['*'] + } + }; + + const fileUris = await vscode.window.showOpenDialog(options); + if (fileUris && fileUris.length > 0) { + return fileUris.map(uri => uri.fsPath); + } + + return []; + } + + /** + * 检查文件是否存在 + */ + static async fileExists(filePath: string): Promise { + try { + await fs.promises.access(filePath, fs.constants.F_OK); + return true; + } catch { + return false; + } + } + + /** + * 获取文件名(不含路径) + */ + static getFileName(filePath: string): string { + return path.basename(filePath); + } + + /** + * 获取相对路径(相对于工作区) + */ + static getRelativePath(filePath: string): string { + const workspaceFolder = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)); + if (workspaceFolder) { + return path.relative(workspaceFolder.uri.fsPath, filePath); + } + return path.basename(filePath); + } +} \ No newline at end of file diff --git a/tools/rst-optimizer/src/utils/wrap.ts b/tools/rst-optimizer/src/utils/wrap.ts new file mode 100644 index 0000000000..5a3cd07535 --- /dev/null +++ b/tools/rst-optimizer/src/utils/wrap.ts @@ -0,0 +1,112 @@ +export class TextWrapper { + /** + * 对文本进行硬换行包裹 + * @param text 原始文本 + * @param width 行宽限制 + * @returns 包裹后的文本 + */ + static wrapText(text: string, width: number): string { + if (width <= 0) { + return text; + } + + const lines = text.split('\n'); + const wrappedLines: string[] = []; + + for (const line of lines) { + // 保留空行 + if (line.trim() === '') { + wrappedLines.push(line); + continue; + } + + // 检查是否是 RST 特殊行(不应该被包裹) + if (this.shouldPreserveLine(line)) { + wrappedLines.push(line); + continue; + } + + // 对普通文本行进行包裹 + const wrapped = this.wrapLine(line, width); + wrappedLines.push(...wrapped); + } + + return wrappedLines.join('\n'); + } + + /** + * 检查行是否应该保持原样(不进行包裹) + */ + private static shouldPreserveLine(line: string): boolean { + const trimmed = line.trim(); + + // RST 指令行 + if (trimmed.startsWith('.. ')) { + return true; + } + + // 标题下划线 + if (/^[\s]*[=\-`:'~^_*+#<>"]{3,}[\s]*$/.test(trimmed)) { + return true; + } + + // 代码块内容(通过缩进判断) + if (line.startsWith(' ') || line.startsWith('\t')) { + return true; + } + + // 列表项 + if (/^[\s]*[-*+]\s/.test(line) || /^[\s]*\d+\.\s/.test(line)) { + return true; + } + + // 表格行 + if (trimmed.includes('|') && trimmed.length > 10) { + return true; + } + + // 链接定义 + if (/^[\s]*\.\. _[^:]+:/.test(line)) { + return true; + } + + return false; + } + + /** + * 包裹单行文本 + */ + private static wrapLine(line: string, width: number): string[] { + const leadingWhitespace = line.match(/^(\s*)/)?.[1] || ''; + const content = line.trim(); + + if (content.length <= width - leadingWhitespace.length) { + return [line]; + } + + const words = content.split(/\s+/); + const wrappedLines: string[] = []; + let currentLine = leadingWhitespace; + + for (const word of words) { + const testLine = currentLine === leadingWhitespace + ? currentLine + word + : currentLine + ' ' + word; + + if (testLine.length <= width) { + currentLine = testLine; + } else { + if (currentLine.trim()) { + wrappedLines.push(currentLine); + } + currentLine = leadingWhitespace + word; + } + } + + if (currentLine.trim()) { + wrappedLines.push(currentLine); + } + + return wrappedLines.length > 0 ? wrappedLines : [line]; + } +} \ No newline at end of file diff --git a/tools/rst-optimizer/src/views/batchResultsHtml.ts b/tools/rst-optimizer/src/views/batchResultsHtml.ts new file mode 100644 index 0000000000..bcbe9b63e6 --- /dev/null +++ b/tools/rst-optimizer/src/views/batchResultsHtml.ts @@ -0,0 +1,122 @@ +import { FileUtils } from '../utils/file'; + +export interface BatchResultItem { + id: string; + timestamp: number; + filePath: string; + originalText: string; + optimizedText: string; +} + +export function getBatchResultsHtml(results: BatchResultItem[]): string { + const fileItems = results.map((result, index) => { + const fileName = FileUtils.getFileName(result.filePath); + const relativePath = FileUtils.getRelativePath(result.filePath); + const originalLength = result.originalText.length; + const optimizedLength = result.optimizedText.length; + const changePercent = Math.round(((optimizedLength - originalLength) / Math.max(1, originalLength)) * 100); + const date = new Date(result.timestamp || Date.now()); + const timeStr = `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; + + return ` + ++ `; + }).join(''); + + return ` + + + + + ++ + +++ ${relativePath} + ${timeStr} +++ 原文: ${originalLength} 字符 + 优化后: ${optimizedLength} 字符 + + 变化: ${changePercent >= 0 ? '+' : ''}${changePercent}% + ++RST 批量优化结果 + + + +++RST 批量优化结果+成功优化了 ${results.length} 个文件,请查看结果并选择要应用的文件++ + ++${fileItems}++ + + + ++ + + + `; +} diff --git a/tools/rst-optimizer/src/views/batchResultsView.ts b/tools/rst-optimizer/src/views/batchResultsView.ts new file mode 100644 index 0000000000..bee5b38086 --- /dev/null +++ b/tools/rst-optimizer/src/views/batchResultsView.ts @@ -0,0 +1,92 @@ +import * as vscode from 'vscode'; +import { getBatchResultsHtml, BatchResultItem } from './batchResultsHtml'; +import { HistoryStore } from '../history'; + +export type BatchResultsHandlers = { + onApplyAll: (results: BatchResultItem[]) => Promise; + onApplySelected: (results: BatchResultItem[], selectedFiles: string[]) => Promise ; + onViewDiffById: (id: string) => Promise ; + onDiscard: () => Promise ; +}; + +export class BatchResultsViewProvider implements vscode.WebviewViewProvider { + public static readonly ViewId = 'rstOptimizer.batchResults'; + + private _view?: vscode.WebviewView; + private _results: BatchResultItem[] = []; + private _handlers: BatchResultsHandlers; + + constructor(handlers: BatchResultsHandlers) { + this._handlers = handlers; + } + + resolveWebviewView(webviewView: vscode.WebviewView): void | Thenable { + this._view = webviewView; + webviewView.webview.options = { enableScripts: true }; + this._results = HistoryStore.getAll(); + this.updateHtml(); + + webviewView.webview.onDidReceiveMessage(async message => { + switch (message.command) { + case 'applyAll': + await this._handlers.onApplyAll(this._results); + break; + case 'applySelected': + await this._handlers.onApplySelected(this._results, message.selectedFiles || []); + break; + case 'viewDiff': + if (message.id) { + await this._handlers.onViewDiffById(message.id); + } + break; + case 'deleteItem': + if (message.id) { + HistoryStore.removeById(message.id); + this._results = HistoryStore.getAll(); + this.updateHtml(); + } + break; + case 'deleteSelected': + if (Array.isArray(message.ids) && message.ids.length) { + for (const id of message.ids) { + HistoryStore.removeById(id); + } + this._results = HistoryStore.getAll(); + this.updateHtml(); + } + break; + case 'discard': + await this._handlers.onDiscard(); + break; + } + }); + } + + async showResults(results: Array >) { + // Append into history store and refresh from it + if (results && results.length) { + // results may not carry id/timestamp when passed in; ensure persistence creates them + const plain = results.map(r => ({ filePath: r.filePath, originalText: r.originalText, optimizedText: r.optimizedText })); + HistoryStore.addMany(plain); + } + this._results = HistoryStore.getAll(); + this.updateHtml(); + await vscode.commands.executeCommand('workbench.view.explorer'); + await vscode.commands.executeCommand('workbench.view.extension.rstOptimizer'); // in case moved later + await vscode.commands.executeCommand('rstOptimizer.revealBatchResultsView'); + } + + private updateHtml() { + if (!this._view) return; + this._view.webview.html = getBatchResultsHtml(this._results); + } + + reveal() { + this._view?.show?.(true); + } + + refreshFromStore() { + this._results = HistoryStore.getAll(); + this.updateHtml(); + } +} diff --git a/tools/rst-optimizer/test-sample.rst b/tools/rst-optimizer/test-sample.rst new file mode 100644 index 0000000000..1a2508c06c --- /dev/null +++ b/tools/rst-optimizer/test-sample.rst @@ -0,0 +1,28 @@ +测试文档 +======== + +这是一个测试文档。它有一些问题需要优化。 + +功能介绍 +-------- + +这个函数很有用。它可以做很多事情。但是描述不够清楚。 + +.. code-block:: python + + def test_function(): + # 这是一个测试函数 + pass + +参考链接 +-------- + +参考 :ref:`other-section` 了解更多。 + +.. note:: + 这是一个注意事项。 + +结论 +---- + +总之,这个文档需要优化。 \ No newline at end of file diff --git a/tools/rst-optimizer/test/runTest.js b/tools/rst-optimizer/test/runTest.js new file mode 100644 index 0000000000..a23c16eed2 --- /dev/null +++ b/tools/rst-optimizer/test/runTest.js @@ -0,0 +1,53 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +Object.defineProperty(exports, "__esModule", { value: true }); +const path = __importStar(require("path")); +const test_electron_1 = require("@vscode/test-electron"); +async function main() { + try { + // 扩展开发文件夹路径 + const extensionDevelopmentPath = path.resolve(__dirname, '../../'); + // 测试套件路径 + const extensionTestsPath = path.resolve(__dirname, './suite/index'); + // 下载 VS Code,解压并运行集成测试 + await (0, test_electron_1.runTests)({ extensionDevelopmentPath, extensionTestsPath }); + } + catch (err) { + console.error('测试运行失败'); + process.exit(1); + } +} +main(); +//# sourceMappingURL=runTest.js.map \ No newline at end of file diff --git a/tools/rst-optimizer/test/runTest.js.map b/tools/rst-optimizer/test/runTest.js.map new file mode 100644 index 0000000000..8ce2d31a11 --- /dev/null +++ b/tools/rst-optimizer/test/runTest.js.map @@ -0,0 +1 @@ +{"version":3,"file":"runTest.js","sourceRoot":"","sources":["runTest.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,2CAA6B;AAE7B,yDAAiD;AAEjD,KAAK,UAAU,IAAI;IACjB,IAAI,CAAC;QACH,YAAY;QACZ,MAAM,wBAAwB,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;QAEnE,SAAS;QACT,MAAM,kBAAkB,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;QAEpE,uBAAuB;QACvB,MAAM,IAAA,wBAAQ,EAAC,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,CAAC,CAAC;IACnE,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QACxB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC;AAED,IAAI,EAAE,CAAC"} \ No newline at end of file diff --git a/tools/rst-optimizer/test/runTest.ts b/tools/rst-optimizer/test/runTest.ts new file mode 100644 index 0000000000..ec6084df1e --- /dev/null +++ b/tools/rst-optimizer/test/runTest.ts @@ -0,0 +1,21 @@ +import * as path from 'path'; + +import { runTests } from '@vscode/test-electron'; + +async function main() { + try { + // 扩展开发文件夹路径 + const extensionDevelopmentPath = path.resolve(__dirname, '../../'); + + // 测试套件路径 + const extensionTestsPath = path.resolve(__dirname, './suite/index'); + + // 下载 VS Code,解压并运行集成测试 + await runTests({ extensionDevelopmentPath, extensionTestsPath }); + } catch (err) { + console.error('测试运行失败'); + process.exit(1); + } +} + +main(); \ No newline at end of file diff --git a/tools/rst-optimizer/test/suite/extension.test.ts b/tools/rst-optimizer/test/suite/extension.test.ts new file mode 100644 index 0000000000..25a52ba337 --- /dev/null +++ b/tools/rst-optimizer/test/suite/extension.test.ts @@ -0,0 +1,71 @@ +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { ConfigManager } from '../../src/config'; + +suite('扩展测试套件', () => { + vscode.window.showInformationMessage('开始运行所有测试。'); + + test('扩展应该被激活', async () => { + const extension = vscode.extensions.getExtension('undefined_publisher.rst-optimizer'); + assert.ok(extension); + + if (!extension.isActive) { + await extension.activate(); + } + + assert.ok(extension.isActive); + }); + + test('所有命令应该被注册', async () => { + const commands = await vscode.commands.getCommands(true); + + const expectedCommands = [ + 'rstOptimizer.optimizeCurrent', + 'rstOptimizer.optimizePickFile', + 'rstOptimizer.applyResult', + 'rstOptimizer.openSettings', + 'rstOptimizer.showQuickPick' + ]; + + for (const command of expectedCommands) { + assert.ok(commands.includes(command), `命令 ${command} 应该被注册`); + } + }); +}); + +suite('配置管理测试', () => { + test('应该能够读取默认配置', () => { + const config = ConfigManager.getConfig(); + + assert.strictEqual(config.provider, 'openai-compatible'); + assert.strictEqual(config.baseUrl, 'https://api.openai.com/v1'); + assert.strictEqual(config.model, 'gpt-4o-mini'); + assert.strictEqual(config.maxTokens, 4096); + assert.strictEqual(config.temperature, 0.3); + assert.strictEqual(config.neverUploadIfWorkspaceTrusted, false); + assert.strictEqual(config.rewrapWidth, 0); + }); + + test('应该验证配置错误', () => { + const invalidConfig = { + provider: 'openai-compatible', + baseUrl: '', + model: '', + apiKey: '', + userPrompt: '测试提示', + maxTokens: -1, + temperature: 3, + neverUploadIfWorkspaceTrusted: false, + rewrapWidth: 0 + }; + + const errors = ConfigManager.validateConfig(invalidConfig); + + assert.ok(errors.length > 0, '应该检测到配置错误'); + assert.ok(errors.some(e => e.includes('API 基础 URL')), '应该检测到空的基础 URL'); + assert.ok(errors.some(e => e.includes('模型名称')), '应该检测到空的模型名称'); + assert.ok(errors.some(e => e.includes('API 密钥')), '应该检测到空的 API 密钥'); + assert.ok(errors.some(e => e.includes('最大 token')), '应该检测到无效的最大 token'); + assert.ok(errors.some(e => e.includes('温度值')), '应该检测到无效的温度值'); + }); +}); \ No newline at end of file diff --git a/tools/rst-optimizer/test/suite/index.ts b/tools/rst-optimizer/test/suite/index.ts new file mode 100644 index 0000000000..9db559f8aa --- /dev/null +++ b/tools/rst-optimizer/test/suite/index.ts @@ -0,0 +1,36 @@ +import * as path from 'path'; +import { glob } from 'glob'; + +export function run(): Promise { + const testsRoot = path.resolve(__dirname, '..'); + + return new Promise((resolve, reject) => { + glob('**/**.test.js', { cwd: testsRoot }) + .then((files: string[]) => { + // 动态导入 Mocha + const Mocha = require('mocha'); + const mocha = new Mocha({ + ui: 'tdd', + color: true + }); + + // 添加文件到测试套件 + files.forEach((f: string) => mocha.addFile(path.resolve(testsRoot, f))); + + try { + // 运行 Mocha 测试 + mocha.run((failures: number) => { + if (failures > 0) { + reject(new Error(`${failures} 个测试失败。`)); + } else { + resolve(); + } + }); + } catch (err) { + console.error(err); + reject(err); + } + }) + .catch(reject); + }); +} diff --git a/tools/rst-optimizer/test/suite/virtualDoc.test.ts b/tools/rst-optimizer/test/suite/virtualDoc.test.ts new file mode 100644 index 0000000000..4b06108d0a --- /dev/null +++ b/tools/rst-optimizer/test/suite/virtualDoc.test.ts @@ -0,0 +1,73 @@ +import * as assert from 'assert'; +import * as vscode from 'vscode'; +import { VirtualDocProvider } from '../../src/diff/virtualDocProvider'; + +suite('虚拟文档提供者测试', () => { + let provider: VirtualDocProvider; + + setup(() => { + provider = new VirtualDocProvider(); + }); + + test('应该能够创建 URI', () => { + const filePath = '/test/file.rst'; + const originalUri = VirtualDocProvider.createUri('original', filePath); + const optimizedUri = VirtualDocProvider.createUri('optimized', filePath); + + assert.ok(originalUri.scheme === VirtualDocProvider.getScheme()); + assert.ok(optimizedUri.scheme === VirtualDocProvider.getScheme()); + assert.ok(originalUri.path.includes('original')); + assert.ok(optimizedUri.path.includes('optimized')); + assert.ok(originalUri.path.includes(encodeURIComponent(filePath))); + assert.ok(optimizedUri.path.includes(encodeURIComponent(filePath))); + }); + + test('应该能够设置和获取内容', () => { + const filePath = '/test/file.rst'; + const uri = VirtualDocProvider.createUri('original', filePath); + const content = '这是测试内容'; + + provider.set(uri, content); + const retrievedContent = provider.provideTextDocumentContent(uri); + + assert.strictEqual(retrievedContent, content); + }); + + test('应该能够清除内容', () => { + const filePath = '/test/file.rst'; + const uri = VirtualDocProvider.createUri('original', filePath); + const content = '这是测试内容'; + + provider.set(uri, content); + assert.strictEqual(provider.provideTextDocumentContent(uri), content); + + provider.clear(uri); + assert.strictEqual(provider.provideTextDocumentContent(uri), undefined); + }); + + test('应该能够清除所有内容', () => { + const filePath1 = '/test/file1.rst'; + const filePath2 = '/test/file2.rst'; + const uri1 = VirtualDocProvider.createUri('original', filePath1); + const uri2 = VirtualDocProvider.createUri('optimized', filePath2); + + provider.set(uri1, '内容1'); + provider.set(uri2, '内容2'); + + assert.strictEqual(provider.provideTextDocumentContent(uri1), '内容1'); + assert.strictEqual(provider.provideTextDocumentContent(uri2), '内容2'); + + provider.clearAll(); + + assert.strictEqual(provider.provideTextDocumentContent(uri1), undefined); + assert.strictEqual(provider.provideTextDocumentContent(uri2), undefined); + }); + + test('不存在的 URI 应该返回 undefined', () => { + const filePath = '/test/nonexistent.rst'; + const uri = VirtualDocProvider.createUri('original', filePath); + + const content = provider.provideTextDocumentContent(uri); + assert.strictEqual(content, undefined); + }); +}); \ No newline at end of file diff --git a/tools/rst-optimizer/tsconfig.json b/tools/rst-optimizer/tsconfig.json new file mode 100644 index 0000000000..4fa57348c4 --- /dev/null +++ b/tools/rst-optimizer/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "out", + "lib": [ + "ES2020" + ], + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": [ + "src/**/*", + "test/**/*" + ], + "exclude": [ + "node_modules", + ".vscode-test" + ] +} -- Gitee